mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-24 04:57:00 +01:00
509 oauth2 scope mapping (#586)
This commit is contained in:
parent
d59ddcc74a
commit
c62b39c338
3
Makefile
3
Makefile
|
@ -13,7 +13,6 @@ help:
|
||||||
buildx/kanidmd/simd: ## build multiarch server images
|
buildx/kanidmd/simd: ## build multiarch server images
|
||||||
buildx/kanidmd/simd:
|
buildx/kanidmd/simd:
|
||||||
@docker buildx build $(EXT_OPTS) --pull --push --platform "linux/amd64" \
|
@docker buildx build $(EXT_OPTS) --pull --push --platform "linux/amd64" \
|
||||||
--allow security.insecure \
|
|
||||||
-f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:x86_64_$(IMAGE_VERSION) \
|
-f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:x86_64_$(IMAGE_VERSION) \
|
||||||
--build-arg "KANIDM_BUILD_PROFILE=container_x86_64_v3" \
|
--build-arg "KANIDM_BUILD_PROFILE=container_x86_64_v3" \
|
||||||
--build-arg "KANIDM_FEATURES=simd_support" \
|
--build-arg "KANIDM_FEATURES=simd_support" \
|
||||||
|
@ -23,7 +22,6 @@ buildx/kanidmd/simd:
|
||||||
buildx/kanidmd/x86_64_v3: ## build multiarch server images
|
buildx/kanidmd/x86_64_v3: ## build multiarch server images
|
||||||
buildx/kanidmd/x86_64_v3:
|
buildx/kanidmd/x86_64_v3:
|
||||||
@docker buildx build $(EXT_OPTS) --pull --push --platform "linux/amd64" \
|
@docker buildx build $(EXT_OPTS) --pull --push --platform "linux/amd64" \
|
||||||
--allow security.insecure \
|
|
||||||
-f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:x86_64_$(IMAGE_VERSION) \
|
-f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:x86_64_$(IMAGE_VERSION) \
|
||||||
--build-arg "KANIDM_BUILD_PROFILE=container_x86_64_v3" \
|
--build-arg "KANIDM_BUILD_PROFILE=container_x86_64_v3" \
|
||||||
--build-arg "KANIDM_FEATURES=" \
|
--build-arg "KANIDM_FEATURES=" \
|
||||||
|
@ -33,7 +31,6 @@ buildx/kanidmd/x86_64_v3:
|
||||||
buildx/kanidmd: ## build multiarch server images
|
buildx/kanidmd: ## build multiarch server images
|
||||||
buildx/kanidmd:
|
buildx/kanidmd:
|
||||||
@docker buildx build $(EXT_OPTS) --pull --push --platform $(IMAGE_ARCH) \
|
@docker buildx build $(EXT_OPTS) --pull --push --platform $(IMAGE_ARCH) \
|
||||||
--allow security.insecure \
|
|
||||||
-f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:$(IMAGE_VERSION) \
|
-f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:$(IMAGE_VERSION) \
|
||||||
--build-arg "KANIDM_BUILD_PROFILE=container_generic" \
|
--build-arg "KANIDM_BUILD_PROFILE=container_generic" \
|
||||||
--build-arg "KANIDM_FEATURES=" \
|
--build-arg "KANIDM_FEATURES=" \
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
- [POSIX Accounts and Groups](./posix_accounts.md)
|
- [POSIX Accounts and Groups](./posix_accounts.md)
|
||||||
- [SSH Key Distribution](./ssh_key_dist.md)
|
- [SSH Key Distribution](./ssh_key_dist.md)
|
||||||
- [The Recycle Bin](./recycle_bin.md)
|
- [The Recycle Bin](./recycle_bin.md)
|
||||||
- [LDAP](./ldap.md)
|
- [Oauth2](./oauth2.md)
|
||||||
- [PAM and nsswitch](./pam_and_nsswitch.md)
|
- [PAM and nsswitch](./pam_and_nsswitch.md)
|
||||||
- [RADIUS](./radius.md)
|
- [RADIUS](./radius.md)
|
||||||
|
- [LDAP](./ldap.md)
|
||||||
- [Why TLS?](./why_tls.md)
|
- [Why TLS?](./why_tls.md)
|
||||||
|
|
|
@ -4,7 +4,7 @@ The monitoring design of Kanidm is still very much in its infancy - [take part i
|
||||||
|
|
||||||
## kanidmd
|
## kanidmd
|
||||||
|
|
||||||
kanidmd currently responds to HTTP GET requests at the `/status` endpoint with a JSON object of either "true" or "false". `true` indicates that the platform is responding to requests.
|
kanidmd currently responds to HTTP GET requests at the `/status` endpoint with a JSON object of either "true" or "false". `true` indicates that the platform is responding to requests.
|
||||||
|
|
||||||
| URL | `<hostname>/status` |
|
| URL | `<hostname>/status` |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|
136
kanidm_book/src/oauth2.md
Normal file
136
kanidm_book/src/oauth2.md
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
# Oauth2
|
||||||
|
|
||||||
|
Oauth is a web authorisation protocol that allows "single sign on". It's key to note
|
||||||
|
oauth is authorisation, not authentication, as the protocol in it's default forms
|
||||||
|
do not provide identity or authentication information, only information that
|
||||||
|
an entity is authorised for the requested resources.
|
||||||
|
|
||||||
|
Oauth can tie into extensions allowing an identity provider to reveal information
|
||||||
|
about authorised sessions. This extends oauth from an authorisation only system
|
||||||
|
to a system capable of identity and authorisation. Two primary methods of this
|
||||||
|
exist today: rfc7662 token introspection, and openid connect.
|
||||||
|
|
||||||
|
## How Does Oauth2 Work?
|
||||||
|
|
||||||
|
A user wishes to access a service (resource, resource server). The resource
|
||||||
|
server does not have an active session for the client, so it redirects to the
|
||||||
|
authorisation server (Kanidm) to determine if the client should be allowed to proceed, and
|
||||||
|
has the appropriate permissions (scopes) for the requested resources.
|
||||||
|
|
||||||
|
The authorisation server checks the current session of the user and may present
|
||||||
|
a login flow if required. Given the identity of the user known to the authorisation
|
||||||
|
sever, and the requested scopes, the authorisation server makes a decision if it
|
||||||
|
allows the authorisation to proceed. The user is then prompted to consent to the
|
||||||
|
authorisation from the authorisation server to the resource server as some identity
|
||||||
|
information may be revealed by granting this consent.
|
||||||
|
|
||||||
|
If successful and consent given, the user is redirected back to the resource server with an authorisation
|
||||||
|
code. The resource server then contacts the authorisation server directly with this
|
||||||
|
code and exchanges it for a valid token that may be provided to the users browser.
|
||||||
|
|
||||||
|
The resource server may then optionally contact the token introspection endpoint of the authorisation server about the
|
||||||
|
provided oauth token, which yields extra metadata about the identity that holds the
|
||||||
|
token from the authorisation. This metadata may include identity information,
|
||||||
|
but also may include extended metadata, sometimes refered to as "claims". Claims are
|
||||||
|
information bound to a token based on properties of the session that may allow
|
||||||
|
the resource server to make extended authorisation decisions without the need
|
||||||
|
to contact the authorisation server to arbitrate.
|
||||||
|
|
||||||
|
It's important to note that oauth2 at it's core is an authorisation system which has layered
|
||||||
|
identity providing elements on top.
|
||||||
|
|
||||||
|
### Resource Server
|
||||||
|
|
||||||
|
This is the server that a user wants to access. Common examples could be nextcloud, a wiki
|
||||||
|
or something else. This is the system that "needs protecting" and wants to delegate authorisation
|
||||||
|
decisions to Kanidm.
|
||||||
|
|
||||||
|
It's important for you to know *how* your resource server supports oauth2. For example, does it
|
||||||
|
support rfc7662 token introspection or does it rely on openid connect for identity information?
|
||||||
|
Does the resource server support PKCE or not?
|
||||||
|
|
||||||
|
In general Kanidm requires that your resource server supports:
|
||||||
|
|
||||||
|
* HTTP basic authentication to the authorisation server
|
||||||
|
* PKCE code verification to prevent certain token attack classes
|
||||||
|
|
||||||
|
Kanidm will expose it's oauth2 apis at the urls:
|
||||||
|
|
||||||
|
* auth url: https://idm.example.com/ui/oauth2
|
||||||
|
* token url: https://idm.example.com/oauth2/token
|
||||||
|
* token inspect url: https://idm.example.com/oauth2/inspect
|
||||||
|
|
||||||
|
### Scope Relationships
|
||||||
|
|
||||||
|
For an authorisation to proceed, the resource server will request a list of scopes, which are
|
||||||
|
unique to that resource server. For example, when a user wishes to login to the admin panel
|
||||||
|
of the resource server, it may request the "admin" scope from kanidm for authorisation. But when
|
||||||
|
a user wants to login, it may only request "acces" as a scope from kanidm.
|
||||||
|
|
||||||
|
As each resource server may have it's own scopes and understanding of these, Kanidm isolates
|
||||||
|
scopes to each resource server connected to Kanidm. Kanidm has two methods of granting scopes to accounts (users).
|
||||||
|
|
||||||
|
The first are implicit scopes. These are scopes granted to all accounts that Kanidm holds.
|
||||||
|
|
||||||
|
The second is scope mappings. These provide a set of scopes if a user is a member of a specific
|
||||||
|
group within Kanidm. This allows you to create a relationship between the scopes of a resource
|
||||||
|
server, and the groups/roles in Kanidm which can be specific to that resource server.
|
||||||
|
|
||||||
|
For an authorisation to proceed, all scopes requested must be available in the final scope set
|
||||||
|
that is granted to the account. This final scope set can be built from implicit and mapped
|
||||||
|
scopes.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Create the Kanidm Configuration
|
||||||
|
|
||||||
|
After you have understood your resource server requirements you first need to configure Kanidm.
|
||||||
|
By default members of "system\_admins" or "idm\_hp\_oauth2\_manage\_priv" are able to create or
|
||||||
|
manage oauth2 resource server integrations.
|
||||||
|
|
||||||
|
You can create a new resource server with:
|
||||||
|
|
||||||
|
kanidm system oauth2 create <name> <displayname> <origin>
|
||||||
|
kanidm system oauth2 create nextcloud "Nextcloud Production" https://nextcloud.example.com
|
||||||
|
|
||||||
|
If you wish to create implicit scopes you can set these with:
|
||||||
|
|
||||||
|
kanidm system oauth2 set_implicit_scopes <name> [scopes]...
|
||||||
|
kanidm system oauth2 set_implicit_scopes nextcloud login read_user
|
||||||
|
|
||||||
|
You can create a scope map with:
|
||||||
|
|
||||||
|
kanidm system oauth2 create_scope_map <name> <kanidm_group_name> [scopes]...
|
||||||
|
kanidm system oauth2 create_scope_map nextcloud nextcloud_admins admin
|
||||||
|
|
||||||
|
Once created you can view the details of the resource server.
|
||||||
|
|
||||||
|
kanidm system oauth2 get nextcloud
|
||||||
|
---
|
||||||
|
class: oauth2_resource_server
|
||||||
|
class: oauth2_resource_server_basic
|
||||||
|
class: object
|
||||||
|
displayname: Nextcloud Production
|
||||||
|
oauth2_rs_basic_secret: <secret>
|
||||||
|
oauth2_rs_name: nextcloud
|
||||||
|
oauth2_rs_origin: https://nextcloud.example.com
|
||||||
|
oauth2_rs_token_key: hidden
|
||||||
|
|
||||||
|
### Configure the Resource Server
|
||||||
|
|
||||||
|
On your resource server, you should configure the client id as the "oauth2\_rs\_name" from
|
||||||
|
kanidm, and the password to be the value shown in "oauth2\_rs\_basic\_secret"
|
||||||
|
|
||||||
|
You should now be able to test authorisation.
|
||||||
|
|
||||||
|
## Resetting Resource Server Security Material
|
||||||
|
|
||||||
|
In the case of disclosure of the basic secret, or some other security event where you may wish
|
||||||
|
to invalidate a resource servers active sessions/tokens, you can reset the secret material of
|
||||||
|
the server with:
|
||||||
|
|
||||||
|
kanidm system oauth2 reset_secrets
|
||||||
|
|
||||||
|
Each resource server has unique signing keys and access secrets, so this is limited to each
|
||||||
|
resource server.
|
||||||
|
|
|
@ -40,9 +40,10 @@ An entry can be revived with:
|
||||||
|
|
||||||
The recycle bin is a best effort to restore your data - there are some cases where
|
The recycle bin is a best effort to restore your data - there are some cases where
|
||||||
the revived entries may not be the same as their were when they were deleted. This
|
the revived entries may not be the same as their were when they were deleted. This
|
||||||
generally revolves around reference types such as group membership.
|
generally revolves around reference types such as group membership, or when the reference
|
||||||
|
type includes supplemental map data such as the oauth2 scope map type.
|
||||||
|
|
||||||
An example of this is the following steps:
|
An example of this data loss is the following steps:
|
||||||
|
|
||||||
add user1
|
add user1
|
||||||
add group1
|
add group1
|
||||||
|
@ -63,7 +64,7 @@ membership of user1 in group1 would be lost in this process. To explain why:
|
||||||
revive user1 // re-add groups based on directmemberof (empty set)
|
revive user1 // re-add groups based on directmemberof (empty set)
|
||||||
revive group1 // no members
|
revive group1 // no members
|
||||||
|
|
||||||
This issue could be looked at again in the future, but for now we think that deletes of
|
These issues could be looked at again in the future, but for now we think that deletes of
|
||||||
groups is rare - we expect recycle bin to save you in "opps" moments, and in a majority
|
groups is rare - we expect recycle bin to save you in "opps" moments, and in a majority
|
||||||
of cases you may delete a group or a user and then restore them. To handle this series
|
of cases you may delete a group or a user and then restore them. To handle this series
|
||||||
of steps requires extra code complexity in how we flag operations. For more,
|
of steps requires extra code complexity in how we flag operations. For more,
|
||||||
|
|
|
@ -1378,12 +1378,16 @@ impl KanidmAsyncClient {
|
||||||
pub async fn idm_oauth2_rs_basic_create(
|
pub async fn idm_oauth2_rs_basic_create(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
|
displayname: &str,
|
||||||
origin: &str,
|
origin: &str,
|
||||||
) -> Result<(), ClientError> {
|
) -> Result<(), ClientError> {
|
||||||
let mut new_oauth2_rs = Entry::default();
|
let mut new_oauth2_rs = Entry::default();
|
||||||
new_oauth2_rs
|
new_oauth2_rs
|
||||||
.attrs
|
.attrs
|
||||||
.insert("oauth2_rs_name".to_string(), vec![name.to_string()]);
|
.insert("oauth2_rs_name".to_string(), vec![name.to_string()]);
|
||||||
|
new_oauth2_rs
|
||||||
|
.attrs
|
||||||
|
.insert("displayname".to_string(), vec![displayname.to_string()]);
|
||||||
new_oauth2_rs
|
new_oauth2_rs
|
||||||
.attrs
|
.attrs
|
||||||
.insert("oauth2_rs_origin".to_string(), vec![origin.to_string()]);
|
.insert("oauth2_rs_origin".to_string(), vec![origin.to_string()]);
|
||||||
|
@ -1400,7 +1404,9 @@ impl KanidmAsyncClient {
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
name: Option<&str>,
|
name: Option<&str>,
|
||||||
|
displayname: Option<&str>,
|
||||||
origin: Option<&str>,
|
origin: Option<&str>,
|
||||||
|
scopes: Option<Vec<&str>>,
|
||||||
reset_secret: bool,
|
reset_secret: bool,
|
||||||
reset_token_key: bool,
|
reset_token_key: bool,
|
||||||
) -> Result<(), ClientError> {
|
) -> Result<(), ClientError> {
|
||||||
|
@ -1413,11 +1419,22 @@ impl KanidmAsyncClient {
|
||||||
.attrs
|
.attrs
|
||||||
.insert("oauth2_rs_name".to_string(), vec![newname.to_string()]);
|
.insert("oauth2_rs_name".to_string(), vec![newname.to_string()]);
|
||||||
}
|
}
|
||||||
|
if let Some(newdisplayname) = displayname {
|
||||||
|
update_oauth2_rs
|
||||||
|
.attrs
|
||||||
|
.insert("displayname".to_string(), vec![newdisplayname.to_string()]);
|
||||||
|
}
|
||||||
if let Some(neworigin) = origin {
|
if let Some(neworigin) = origin {
|
||||||
update_oauth2_rs
|
update_oauth2_rs
|
||||||
.attrs
|
.attrs
|
||||||
.insert("oauth2_rs_origin".to_string(), vec![neworigin.to_string()]);
|
.insert("oauth2_rs_origin".to_string(), vec![neworigin.to_string()]);
|
||||||
}
|
}
|
||||||
|
if let Some(newscopes) = scopes {
|
||||||
|
update_oauth2_rs.attrs.insert(
|
||||||
|
"oauth2_rs_implicit_scopes".to_string(),
|
||||||
|
newscopes.into_iter().map(str::to_string).collect(),
|
||||||
|
);
|
||||||
|
}
|
||||||
if reset_secret {
|
if reset_secret {
|
||||||
update_oauth2_rs
|
update_oauth2_rs
|
||||||
.attrs
|
.attrs
|
||||||
|
@ -1426,13 +1443,36 @@ impl KanidmAsyncClient {
|
||||||
if reset_token_key {
|
if reset_token_key {
|
||||||
update_oauth2_rs
|
update_oauth2_rs
|
||||||
.attrs
|
.attrs
|
||||||
.insert("oauth2_rs_basic_token_key".to_string(), Vec::new());
|
.insert("oauth2_rs_token_key".to_string(), Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
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_create_scope_map(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
group: &str,
|
||||||
|
scopes: Vec<&str>,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
let scopes: Vec<String> = scopes.into_iter().map(str::to_string).collect();
|
||||||
|
self.perform_post_request(
|
||||||
|
format!("/v1/oauth2/{}/_scopemap/{}", id, group).as_str(),
|
||||||
|
scopes,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn idm_oauth2_rs_delete_scope_map(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
group: &str,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
self.perform_delete_request(format!("/v1/oauth2/{}/_scopemap/{}", id, group).as_str())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn idm_oauth2_rs_delete(&self, id: &str) -> Result<(), ClientError> {
|
pub async fn idm_oauth2_rs_delete(&self, id: &str) -> Result<(), ClientError> {
|
||||||
self.perform_delete_request(["/v1/oauth2/", id].concat().as_str())
|
self.perform_delete_request(["/v1/oauth2/", id].concat().as_str())
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -891,8 +891,16 @@ impl KanidmClient {
|
||||||
tokio_block_on(self.asclient.idm_oauth2_rs_list())
|
tokio_block_on(self.asclient.idm_oauth2_rs_list())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn idm_oauth2_rs_basic_create(&self, name: &str, origin: &str) -> Result<(), ClientError> {
|
pub fn idm_oauth2_rs_basic_create(
|
||||||
tokio_block_on(self.asclient.idm_oauth2_rs_basic_create(name, origin))
|
&self,
|
||||||
|
name: &str,
|
||||||
|
displayname: &str,
|
||||||
|
origin: &str,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
tokio_block_on(
|
||||||
|
self.asclient
|
||||||
|
.idm_oauth2_rs_basic_create(name, displayname, origin),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn idm_oauth2_rs_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
|
pub fn idm_oauth2_rs_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
|
||||||
|
@ -903,19 +911,39 @@ impl KanidmClient {
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
name: Option<&str>,
|
name: Option<&str>,
|
||||||
|
displayname: Option<&str>,
|
||||||
origin: Option<&str>,
|
origin: Option<&str>,
|
||||||
|
scopes: Option<Vec<&str>>,
|
||||||
reset_secret: bool,
|
reset_secret: bool,
|
||||||
reset_token_key: bool,
|
reset_token_key: bool,
|
||||||
) -> Result<(), ClientError> {
|
) -> Result<(), ClientError> {
|
||||||
tokio_block_on(self.asclient.idm_oauth2_rs_update(
|
tokio_block_on(self.asclient.idm_oauth2_rs_update(
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
displayname,
|
||||||
origin,
|
origin,
|
||||||
|
scopes,
|
||||||
reset_secret,
|
reset_secret,
|
||||||
reset_token_key,
|
reset_token_key,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn idm_oauth2_rs_create_scope_map(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
group: &str,
|
||||||
|
scopes: Vec<&str>,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
tokio_block_on(
|
||||||
|
self.asclient
|
||||||
|
.idm_oauth2_rs_create_scope_map(id, group, scopes),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn idm_oauth2_rs_delete_scope_map(&self, id: &str, group: &str) -> Result<(), ClientError> {
|
||||||
|
tokio_block_on(self.asclient.idm_oauth2_rs_delete_scope_map(id, group))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn idm_oauth2_rs_delete(&self, id: &str) -> Result<(), ClientError> {
|
pub fn idm_oauth2_rs_delete(&self, id: &str) -> Result<(), ClientError> {
|
||||||
tokio_block_on(self.asclient.idm_oauth2_rs_delete(id))
|
tokio_block_on(self.asclient.idm_oauth2_rs_delete(id))
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,9 +39,25 @@ fn test_oauth2_basic_flow() {
|
||||||
|
|
||||||
// Create an oauth2 application integration.
|
// Create an oauth2 application integration.
|
||||||
rsclient
|
rsclient
|
||||||
.idm_oauth2_rs_basic_create("test_integration", "https://demo.example.com")
|
.idm_oauth2_rs_basic_create(
|
||||||
|
"test_integration",
|
||||||
|
"Test Integration",
|
||||||
|
"https://demo.example.com",
|
||||||
|
)
|
||||||
.expect("Failed to create oauth2 config");
|
.expect("Failed to create oauth2 config");
|
||||||
|
|
||||||
|
rsclient
|
||||||
|
.idm_oauth2_rs_update(
|
||||||
|
"test_integration",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(vec!["read", "email"]),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.expect("Failed to update oauth2 config");
|
||||||
|
|
||||||
let oauth2_config = rsclient
|
let oauth2_config = rsclient
|
||||||
.idm_oauth2_rs_get("test_integration")
|
.idm_oauth2_rs_get("test_integration")
|
||||||
.ok()
|
.ok()
|
||||||
|
@ -90,7 +106,7 @@ fn test_oauth2_basic_flow() {
|
||||||
("code_challenge", pkce_code_challenge.as_str()),
|
("code_challenge", pkce_code_challenge.as_str()),
|
||||||
("code_challenge_method", "S256"),
|
("code_challenge_method", "S256"),
|
||||||
("redirect_uri", "https://demo.example.com/oauth2/flow"),
|
("redirect_uri", "https://demo.example.com/oauth2/flow"),
|
||||||
("scope", "mail+name+test"),
|
("scope", "email read"),
|
||||||
])
|
])
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -1073,7 +1073,11 @@ fn test_server_rest_oauth2_basic_lifecycle() {
|
||||||
|
|
||||||
// Create a new oauth2 config
|
// Create a new oauth2 config
|
||||||
rsclient
|
rsclient
|
||||||
.idm_oauth2_rs_basic_create("test_integration", "https://demo.example.com")
|
.idm_oauth2_rs_basic_create(
|
||||||
|
"test_integration",
|
||||||
|
"Test Integration",
|
||||||
|
"https://demo.example.com",
|
||||||
|
)
|
||||||
.expect("Failed to create oauth2 config");
|
.expect("Failed to create oauth2 config");
|
||||||
|
|
||||||
// List, there is what we created.
|
// List, there is what we created.
|
||||||
|
@ -1091,12 +1095,12 @@ fn test_server_rest_oauth2_basic_lifecycle() {
|
||||||
.flatten()
|
.flatten()
|
||||||
.expect("Failed to retrieve test_integration config");
|
.expect("Failed to retrieve test_integration config");
|
||||||
|
|
||||||
|
eprintln!("{:?}", oauth2_config);
|
||||||
|
|
||||||
// What can we see?
|
// What can we see?
|
||||||
assert!(oauth2_config.attrs.contains_key("oauth2_rs_basic_secret"));
|
assert!(oauth2_config.attrs.contains_key("oauth2_rs_basic_secret"));
|
||||||
// This is present, but redacted.
|
// This is present, but redacted.
|
||||||
assert!(oauth2_config
|
assert!(oauth2_config.attrs.contains_key("oauth2_rs_token_key"));
|
||||||
.attrs
|
|
||||||
.contains_key("oauth2_rs_basic_token_key"));
|
|
||||||
|
|
||||||
// Mod delete the secret/key and check them again.
|
// Mod delete the secret/key and check them again.
|
||||||
// Check we can patch the oauth2_rs_name / oauth2_rs_origin
|
// Check we can patch the oauth2_rs_name / oauth2_rs_origin
|
||||||
|
@ -1104,7 +1108,9 @@ fn test_server_rest_oauth2_basic_lifecycle() {
|
||||||
.idm_oauth2_rs_update(
|
.idm_oauth2_rs_update(
|
||||||
"test_integration",
|
"test_integration",
|
||||||
None,
|
None,
|
||||||
|
Some("Test Integration"),
|
||||||
Some("https://new_demo.example.com"),
|
Some("https://new_demo.example.com"),
|
||||||
|
Some(vec!["read", "email"]),
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
@ -1118,6 +1124,34 @@ fn test_server_rest_oauth2_basic_lifecycle() {
|
||||||
|
|
||||||
assert!(oauth2_config_updated != oauth2_config);
|
assert!(oauth2_config_updated != oauth2_config);
|
||||||
|
|
||||||
|
// Check that we can add scope maps and delete them.
|
||||||
|
rsclient
|
||||||
|
.idm_oauth2_rs_create_scope_map("test_integration", "system_admins", vec!["a", "b"])
|
||||||
|
.expect("Failed to create scope map");
|
||||||
|
|
||||||
|
let oauth2_config_updated2 = rsclient
|
||||||
|
.idm_oauth2_rs_get("test_integration")
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.expect("Failed to retrieve test_integration config");
|
||||||
|
|
||||||
|
assert!(oauth2_config_updated != oauth2_config_updated2);
|
||||||
|
|
||||||
|
rsclient
|
||||||
|
.idm_oauth2_rs_delete_scope_map("test_integration", "system_admins")
|
||||||
|
.expect("Failed to delete scope map");
|
||||||
|
|
||||||
|
let oauth2_config_updated3 = rsclient
|
||||||
|
.idm_oauth2_rs_get("test_integration")
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.expect("Failed to retrieve test_integration config");
|
||||||
|
|
||||||
|
eprintln!("{:?}", oauth2_config_updated);
|
||||||
|
eprintln!("{:?}", oauth2_config_updated3);
|
||||||
|
|
||||||
|
assert!(oauth2_config_updated == oauth2_config_updated3);
|
||||||
|
|
||||||
// Delete the config
|
// Delete the config
|
||||||
rsclient
|
rsclient
|
||||||
.idm_oauth2_rs_delete("test_integration")
|
.idm_oauth2_rs_delete("test_integration")
|
||||||
|
|
|
@ -31,6 +31,7 @@ pub struct AuthorisationRequest {
|
||||||
pub struct ConsentRequest {
|
pub struct ConsentRequest {
|
||||||
// A pretty-name of the client
|
// A pretty-name of the client
|
||||||
pub client_name: String,
|
pub client_name: String,
|
||||||
|
// A list of scopes requested / to be issued.
|
||||||
pub scopes: Vec<String>,
|
pub scopes: Vec<String>,
|
||||||
// The users displayname (?)
|
// The users displayname (?)
|
||||||
// pub display_name: String,
|
// pub display_name: String,
|
||||||
|
|
|
@ -6,6 +6,10 @@ impl Oauth2Opt {
|
||||||
Oauth2Opt::List(copt) => copt.debug,
|
Oauth2Opt::List(copt) => copt.debug,
|
||||||
Oauth2Opt::Get(nopt) => nopt.copt.debug,
|
Oauth2Opt::Get(nopt) => nopt.copt.debug,
|
||||||
Oauth2Opt::CreateBasic(cbopt) => cbopt.nopt.copt.debug,
|
Oauth2Opt::CreateBasic(cbopt) => cbopt.nopt.copt.debug,
|
||||||
|
Oauth2Opt::SetImplictScopes(cbopt) => cbopt.nopt.copt.debug,
|
||||||
|
Oauth2Opt::CreateScopeMap(cbopt) => cbopt.nopt.copt.debug,
|
||||||
|
Oauth2Opt::DeleteScopeMap(cbopt) => cbopt.nopt.copt.debug,
|
||||||
|
Oauth2Opt::ResetSecrets(cbopt) => cbopt.copt.debug,
|
||||||
Oauth2Opt::Delete(nopt) => nopt.copt.debug,
|
Oauth2Opt::Delete(nopt) => nopt.copt.debug,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,14 +32,66 @@ impl Oauth2Opt {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Oauth2Opt::CreateBasic(cbopt) => {
|
Oauth2Opt::CreateBasic(cbopt) => {
|
||||||
|
let client = cbopt.nopt.copt.to_client();
|
||||||
|
match client.idm_oauth2_rs_basic_create(
|
||||||
|
cbopt.nopt.name.as_str(),
|
||||||
|
cbopt.displayname.as_str(),
|
||||||
|
cbopt.origin.as_str(),
|
||||||
|
) {
|
||||||
|
Ok(_) => println!("Success"),
|
||||||
|
Err(e) => eprintln!("Error -> {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Oauth2Opt::SetImplictScopes(cbopt) => {
|
||||||
|
let client = cbopt.nopt.copt.to_client();
|
||||||
|
match client.idm_oauth2_rs_update(
|
||||||
|
cbopt.nopt.name.as_str(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(cbopt.scopes.iter().map(|s| s.as_str()).collect()),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
) {
|
||||||
|
Ok(_) => println!("Success"),
|
||||||
|
Err(e) => eprintln!("Error -> {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Oauth2Opt::CreateScopeMap(cbopt) => {
|
||||||
|
let client = cbopt.nopt.copt.to_client();
|
||||||
|
match client.idm_oauth2_rs_create_scope_map(
|
||||||
|
cbopt.nopt.name.as_str(),
|
||||||
|
cbopt.group.as_str(),
|
||||||
|
cbopt.scopes.iter().map(|s| s.as_str()).collect(),
|
||||||
|
) {
|
||||||
|
Ok(_) => println!("Success"),
|
||||||
|
Err(e) => eprintln!("Error -> {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Oauth2Opt::DeleteScopeMap(cbopt) => {
|
||||||
let client = cbopt.nopt.copt.to_client();
|
let client = cbopt.nopt.copt.to_client();
|
||||||
match client
|
match client
|
||||||
.idm_oauth2_rs_basic_create(cbopt.nopt.name.as_str(), cbopt.origin.as_str())
|
.idm_oauth2_rs_delete_scope_map(cbopt.nopt.name.as_str(), cbopt.group.as_str())
|
||||||
{
|
{
|
||||||
Ok(_) => println!("Success"),
|
Ok(_) => println!("Success"),
|
||||||
Err(e) => eprintln!("Error -> {:?}", e),
|
Err(e) => eprintln!("Error -> {:?}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Oauth2Opt::ResetSecrets(cbopt) => {
|
||||||
|
let client = cbopt.copt.to_client();
|
||||||
|
match client.idm_oauth2_rs_update(
|
||||||
|
cbopt.name.as_str(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
) {
|
||||||
|
Ok(_) => println!("Success"),
|
||||||
|
Err(e) => eprintln!("Error -> {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
Oauth2Opt::Delete(nopt) => {
|
Oauth2Opt::Delete(nopt) => {
|
||||||
let client = nopt.copt.to_client();
|
let client = nopt.copt.to_client();
|
||||||
match client.idm_oauth2_rs_delete(nopt.name.as_str()) {
|
match client.idm_oauth2_rs_delete(nopt.name.as_str()) {
|
||||||
|
|
|
@ -354,10 +354,38 @@ pub enum SelfOpt {
|
||||||
|
|
||||||
#[derive(Debug, StructOpt)]
|
#[derive(Debug, StructOpt)]
|
||||||
pub struct Oauth2BasicCreateOpt {
|
pub struct Oauth2BasicCreateOpt {
|
||||||
#[structopt(name = "origin")]
|
|
||||||
origin: String,
|
|
||||||
#[structopt(flatten)]
|
#[structopt(flatten)]
|
||||||
nopt: Named,
|
nopt: Named,
|
||||||
|
#[structopt(name = "displayname")]
|
||||||
|
displayname: String,
|
||||||
|
#[structopt(name = "origin")]
|
||||||
|
origin: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, StructOpt)]
|
||||||
|
pub struct Oauth2SetImplicitScopes {
|
||||||
|
#[structopt(flatten)]
|
||||||
|
nopt: Named,
|
||||||
|
#[structopt(name = "scopes")]
|
||||||
|
scopes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, StructOpt)]
|
||||||
|
pub struct Oauth2CreateScopeMapOpt {
|
||||||
|
#[structopt(flatten)]
|
||||||
|
nopt: Named,
|
||||||
|
#[structopt(name = "group")]
|
||||||
|
group: String,
|
||||||
|
#[structopt(name = "scopes")]
|
||||||
|
scopes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, StructOpt)]
|
||||||
|
pub struct Oauth2DeleteScopeMapOpt {
|
||||||
|
#[structopt(flatten)]
|
||||||
|
nopt: Named,
|
||||||
|
#[structopt(name = "group")]
|
||||||
|
group: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, StructOpt)]
|
#[derive(Debug, StructOpt)]
|
||||||
|
@ -374,6 +402,18 @@ pub enum Oauth2Opt {
|
||||||
#[structopt(name = "create")]
|
#[structopt(name = "create")]
|
||||||
/// Create a new oauth2 resource server
|
/// Create a new oauth2 resource server
|
||||||
CreateBasic(Oauth2BasicCreateOpt),
|
CreateBasic(Oauth2BasicCreateOpt),
|
||||||
|
#[structopt(name = "set_implicit_scopes")]
|
||||||
|
/// Set the list of scopes that are granted to all valid accounts.
|
||||||
|
SetImplictScopes(Oauth2SetImplicitScopes),
|
||||||
|
#[structopt(name = "create_scope_map")]
|
||||||
|
/// Add a new mapping from a group to what scopes it provides
|
||||||
|
CreateScopeMap(Oauth2CreateScopeMapOpt),
|
||||||
|
#[structopt(name = "delete_scope_map")]
|
||||||
|
/// Remove a mapping from groups to scopes
|
||||||
|
DeleteScopeMap(Oauth2DeleteScopeMapOpt),
|
||||||
|
#[structopt(name = "reset_secrets")]
|
||||||
|
/// Reset the secrets associated to this resource server
|
||||||
|
ResetSecrets(Named),
|
||||||
#[structopt(name = "delete")]
|
#[structopt(name = "delete")]
|
||||||
/// Delete a oauth2 resource server
|
/// Delete a oauth2 resource server
|
||||||
Delete(Named),
|
Delete(Named),
|
||||||
|
|
|
@ -1121,6 +1121,129 @@ impl QueryServerWriteV1 {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(
|
||||||
|
level = "trace",
|
||||||
|
name = "oauth2_scopemap_create",
|
||||||
|
skip(self, uat, filter, eventid)
|
||||||
|
fields(uuid = ?eventid)
|
||||||
|
)]
|
||||||
|
pub async fn handle_oauth2_scopemap_create(
|
||||||
|
&self,
|
||||||
|
uat: Option<String>,
|
||||||
|
group: String,
|
||||||
|
scopes: 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 idms_prox_write = self.idms.proxy_write_async(duration_from_epoch_now()).await;
|
||||||
|
spanned!("handle_oauth2_scopemap_create", {
|
||||||
|
let ct = duration_from_epoch_now();
|
||||||
|
|
||||||
|
let ident = idms_prox_write
|
||||||
|
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||||
|
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, 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(
|
||||||
|
"oauth2_rs_scope_map",
|
||||||
|
Value::new_oauthscopemap(group_uuid, scopes.into_iter().collect()),
|
||||||
|
);
|
||||||
|
|
||||||
|
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 = "trace",
|
||||||
|
name = "oauth2_scopemap_delete",
|
||||||
|
skip(self, uat, filter, eventid)
|
||||||
|
fields(uuid = ?eventid)
|
||||||
|
)]
|
||||||
|
pub async fn handle_oauth2_scopemap_delete(
|
||||||
|
&self,
|
||||||
|
uat: Option<String>,
|
||||||
|
group: String,
|
||||||
|
filter: Filter<FilterInvalid>,
|
||||||
|
eventid: Uuid,
|
||||||
|
) -> Result<(), OperationError> {
|
||||||
|
let idms_prox_write = self.idms.proxy_write_async(duration_from_epoch_now()).await;
|
||||||
|
spanned!("handle_oauth2_scopemap_create", {
|
||||||
|
let ct = duration_from_epoch_now();
|
||||||
|
|
||||||
|
let ident = idms_prox_write
|
||||||
|
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||||
|
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, 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(
|
||||||
|
"oauth2_rs_scope_map",
|
||||||
|
PartialValue::new_oauthscopemap(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(|_| ()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ===== These below are internal only event types. =====
|
// ===== These below are internal only event types. =====
|
||||||
#[instrument(
|
#[instrument(
|
||||||
level = "trace",
|
level = "trace",
|
||||||
|
|
|
@ -136,6 +136,14 @@ pub struct DbValueEmailAddressV1 {
|
||||||
pub d: String,
|
pub d: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct DbValueOauthScopeMapV1 {
|
||||||
|
#[serde(rename = "u")]
|
||||||
|
pub refer: Uuid,
|
||||||
|
#[serde(rename = "m")]
|
||||||
|
pub data: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub enum DbValueV1 {
|
pub enum DbValueV1 {
|
||||||
#[serde(rename = "U8")]
|
#[serde(rename = "U8")]
|
||||||
|
@ -176,6 +184,10 @@ pub enum DbValueV1 {
|
||||||
EmailAddress(DbValueEmailAddressV1),
|
EmailAddress(DbValueEmailAddressV1),
|
||||||
#[serde(rename = "UR")]
|
#[serde(rename = "UR")]
|
||||||
Url(Url),
|
Url(Url),
|
||||||
|
#[serde(rename = "OS")]
|
||||||
|
OauthScope(String),
|
||||||
|
#[serde(rename = "OM")]
|
||||||
|
OauthScopeMap(DbValueOauthScopeMapV1),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -1014,33 +1014,41 @@ pub const JSON_IDM_HP_ACP_OAUTH2_MANAGE_PRIV_V1: &str = r#"{
|
||||||
"acp_search_attr": [
|
"acp_search_attr": [
|
||||||
"class",
|
"class",
|
||||||
"description",
|
"description",
|
||||||
|
"displayname",
|
||||||
"oauth2_rs_name",
|
"oauth2_rs_name",
|
||||||
"oauth2_rs_origin",
|
"oauth2_rs_origin",
|
||||||
"oauth2_rs_account_filter",
|
"oauth2_rs_scope_map",
|
||||||
|
"oauth2_rs_implicit_scopes",
|
||||||
"oauth2_rs_basic_secret",
|
"oauth2_rs_basic_secret",
|
||||||
"oauth2_rs_basic_token_key"
|
"oauth2_rs_token_key"
|
||||||
],
|
],
|
||||||
"acp_modify_removedattr": [
|
"acp_modify_removedattr": [
|
||||||
"description",
|
"description",
|
||||||
|
"displayname",
|
||||||
"oauth2_rs_name",
|
"oauth2_rs_name",
|
||||||
"oauth2_rs_origin",
|
"oauth2_rs_origin",
|
||||||
"oauth2_rs_account_filter",
|
"oauth2_rs_scope_map",
|
||||||
|
"oauth2_rs_implicit_scopes",
|
||||||
"oauth2_rs_basic_secret",
|
"oauth2_rs_basic_secret",
|
||||||
"oauth2_rs_basic_token_key"
|
"oauth2_rs_token_key"
|
||||||
],
|
],
|
||||||
"acp_modify_presentattr": [
|
"acp_modify_presentattr": [
|
||||||
"description",
|
"description",
|
||||||
|
"displayname",
|
||||||
"oauth2_rs_name",
|
"oauth2_rs_name",
|
||||||
"oauth2_rs_origin",
|
"oauth2_rs_origin",
|
||||||
"oauth2_rs_account_filter"
|
"oauth2_rs_scope_map",
|
||||||
|
"oauth2_rs_implicit_scopes"
|
||||||
],
|
],
|
||||||
"acp_modify_class": [],
|
"acp_modify_class": [],
|
||||||
"acp_create_attr": [
|
"acp_create_attr": [
|
||||||
"class",
|
"class",
|
||||||
"description",
|
"description",
|
||||||
|
"displayname",
|
||||||
"oauth2_rs_name",
|
"oauth2_rs_name",
|
||||||
"oauth2_rs_origin",
|
"oauth2_rs_origin",
|
||||||
"oauth2_rs_account_filter"
|
"oauth2_rs_scope_map",
|
||||||
|
"oauth2_rs_implicit_scopes"
|
||||||
],
|
],
|
||||||
"acp_create_class": ["oauth2_resource_server", "oauth2_resource_server_basic", "object"]
|
"acp_create_class": ["oauth2_resource_server", "oauth2_resource_server_basic", "object"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -572,7 +572,7 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_ORIGIN: &str = r#"{
|
||||||
}
|
}
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
pub const JSON_SCHEMA_ATTR_OAUTH2_RS_ACCOUNT_FILTER: &str = r#"{
|
pub const JSON_SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP: &str = r#"{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": [
|
"class": [
|
||||||
"object",
|
"object",
|
||||||
|
@ -580,20 +580,22 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_ACCOUNT_FILTER: &str = r#"{
|
||||||
"attributetype"
|
"attributetype"
|
||||||
],
|
],
|
||||||
"description": [
|
"description": [
|
||||||
"A filter describing who may access the associated oauth2 resource server"
|
"A reference to a group mapped to scopes for the associated oauth2 resource server"
|
||||||
|
],
|
||||||
|
"index": [
|
||||||
|
"EQUALITY"
|
||||||
],
|
],
|
||||||
"index": [],
|
|
||||||
"unique": [
|
"unique": [
|
||||||
"false"
|
"false"
|
||||||
],
|
],
|
||||||
"multivalue": [
|
"multivalue": [
|
||||||
"false"
|
"true"
|
||||||
],
|
],
|
||||||
"attributename": [
|
"attributename": [
|
||||||
"oauth2_rs_account_filter"
|
"oauth2_rs_scope_map"
|
||||||
],
|
],
|
||||||
"syntax": [
|
"syntax": [
|
||||||
"JSON_FILTER"
|
"OAUTH_SCOPE_MAP"
|
||||||
],
|
],
|
||||||
"uuid": [
|
"uuid": [
|
||||||
"00000000-0000-0000-0000-ffff00000082"
|
"00000000-0000-0000-0000-ffff00000082"
|
||||||
|
@ -630,7 +632,7 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET: &str = r#"{
|
||||||
}
|
}
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
pub const JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str = r#"{
|
pub const JSON_SCHEMA_ATTR_OAUTH2_RS_TOKEN_KEY: &str = r#"{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": [
|
"class": [
|
||||||
"object",
|
"object",
|
||||||
|
@ -638,7 +640,7 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str = r#"{
|
||||||
"attributetype"
|
"attributetype"
|
||||||
],
|
],
|
||||||
"description": [
|
"description": [
|
||||||
"An oauth2 basic resource servers unique token signing key"
|
"An oauth2 resource servers unique token signing key"
|
||||||
],
|
],
|
||||||
"index": [],
|
"index": [],
|
||||||
"unique": [
|
"unique": [
|
||||||
|
@ -648,7 +650,7 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str = r#"{
|
||||||
"false"
|
"false"
|
||||||
],
|
],
|
||||||
"attributename": [
|
"attributename": [
|
||||||
"oauth2_rs_basic_token_key"
|
"oauth2_rs_token_key"
|
||||||
],
|
],
|
||||||
"syntax": [
|
"syntax": [
|
||||||
"SECRET_UTF8STRING"
|
"SECRET_UTF8STRING"
|
||||||
|
@ -659,6 +661,35 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str = r#"{
|
||||||
}
|
}
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
|
pub const JSON_SCHEMA_ATTR_OAUTH2_RS_IMPLICIT_SCOPES: &str = r#"{
|
||||||
|
"attrs": {
|
||||||
|
"class": [
|
||||||
|
"object",
|
||||||
|
"system",
|
||||||
|
"attributetype"
|
||||||
|
],
|
||||||
|
"description": [
|
||||||
|
"An oauth2 resource servers scopes that are implicitly granted to all users"
|
||||||
|
],
|
||||||
|
"index": [],
|
||||||
|
"unique": [
|
||||||
|
"false"
|
||||||
|
],
|
||||||
|
"multivalue": [
|
||||||
|
"true"
|
||||||
|
],
|
||||||
|
"attributename": [
|
||||||
|
"oauth2_rs_implicit_scopes"
|
||||||
|
],
|
||||||
|
"syntax": [
|
||||||
|
"OAUTH_SCOPE"
|
||||||
|
],
|
||||||
|
"uuid": [
|
||||||
|
"00000000-0000-0000-0000-ffff00000089"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
// === classes ===
|
// === classes ===
|
||||||
|
|
||||||
pub const JSON_SCHEMA_CLASS_PERSON: &str = r#"
|
pub const JSON_SCHEMA_CLASS_PERSON: &str = r#"
|
||||||
|
@ -879,11 +910,14 @@ pub const JSON_SCHEMA_CLASS_OAUTH2_RS: &str = r#"
|
||||||
],
|
],
|
||||||
"systemmay": [
|
"systemmay": [
|
||||||
"description",
|
"description",
|
||||||
"oauth2_rs_account_filter"
|
"oauth2_rs_scope_map",
|
||||||
|
"oauth2_rs_implicit_scopes"
|
||||||
],
|
],
|
||||||
"systemmust": [
|
"systemmust": [
|
||||||
"oauth2_rs_name",
|
"oauth2_rs_name",
|
||||||
"oauth2_rs_origin"
|
"displayname",
|
||||||
|
"oauth2_rs_origin",
|
||||||
|
"oauth2_rs_token_key"
|
||||||
],
|
],
|
||||||
"uuid": [
|
"uuid": [
|
||||||
"00000000-0000-0000-0000-ffff00000085"
|
"00000000-0000-0000-0000-ffff00000085"
|
||||||
|
@ -901,15 +935,14 @@ pub const JSON_SCHEMA_CLASS_OAUTH2_RS_BASIC: &str = r#"
|
||||||
"classtype"
|
"classtype"
|
||||||
],
|
],
|
||||||
"description": [
|
"description": [
|
||||||
"The class representing a configured Oauth2 Resource Server"
|
"The class representing a configured Oauth2 Resource Server authenticated with http basic"
|
||||||
],
|
],
|
||||||
"classname": [
|
"classname": [
|
||||||
"oauth2_resource_server_basic"
|
"oauth2_resource_server_basic"
|
||||||
],
|
],
|
||||||
"systemmay": [],
|
"systemmay": [],
|
||||||
"systemmust": [
|
"systemmust": [
|
||||||
"oauth2_rs_basic_secret",
|
"oauth2_rs_basic_secret"
|
||||||
"oauth2_rs_basic_token_key"
|
|
||||||
],
|
],
|
||||||
"uuid": [
|
"uuid": [
|
||||||
"00000000-0000-0000-0000-ffff00000086"
|
"00000000-0000-0000-0000-ffff00000086"
|
||||||
|
|
|
@ -22,7 +22,7 @@ pub const _STR_UUID_IDM_GROUP_MANAGE_PRIV: &str = "00000000-0000-0000-0000-00000
|
||||||
pub const _STR_UUID_IDM_HP_ACCOUNT_MANAGE_PRIV: &str = "00000000-0000-0000-0000-000000000016";
|
pub const _STR_UUID_IDM_HP_ACCOUNT_MANAGE_PRIV: &str = "00000000-0000-0000-0000-000000000016";
|
||||||
pub const _STR_UUID_IDM_HP_GROUP_MANAGE_PRIV: &str = "00000000-0000-0000-0000-000000000017";
|
pub const _STR_UUID_IDM_HP_GROUP_MANAGE_PRIV: &str = "00000000-0000-0000-0000-000000000017";
|
||||||
pub const STR_UUID_IDM_ADMIN_V1: &str = "00000000-0000-0000-0000-000000000018";
|
pub const STR_UUID_IDM_ADMIN_V1: &str = "00000000-0000-0000-0000-000000000018";
|
||||||
pub const _STR_UUID_SYSTEM_ADMINS: &str = "00000000-0000-0000-0000-000000000019";
|
pub const STR_UUID_SYSTEM_ADMINS: &str = "00000000-0000-0000-0000-000000000019";
|
||||||
pub const STR_UUID_DOMAIN_ADMINS: &str = "00000000-0000-0000-0000-000000000020";
|
pub const STR_UUID_DOMAIN_ADMINS: &str = "00000000-0000-0000-0000-000000000020";
|
||||||
pub const _STR_UUID_IDM_ACCOUNT_UNIX_EXTEND_PRIV: &str = "00000000-0000-0000-0000-000000000021";
|
pub const _STR_UUID_IDM_ACCOUNT_UNIX_EXTEND_PRIV: &str = "00000000-0000-0000-0000-000000000021";
|
||||||
pub const _STR_UUID_IDM_GROUP_UNIX_EXTEND_PRIV: &str = "00000000-0000-0000-0000-000000000022";
|
pub const _STR_UUID_IDM_GROUP_UNIX_EXTEND_PRIV: &str = "00000000-0000-0000-0000-000000000022";
|
||||||
|
@ -134,16 +134,16 @@ pub const STR_UUID_SCHEMA_ATTR_UIDNUMBER: &str = "00000000-0000-0000-0000-ffff00
|
||||||
|
|
||||||
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_NAME: &str = "00000000-0000-0000-0000-ffff00000080";
|
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_NAME: &str = "00000000-0000-0000-0000-ffff00000080";
|
||||||
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_ORIGIN: &str = "00000000-0000-0000-0000-ffff00000081";
|
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_ORIGIN: &str = "00000000-0000-0000-0000-ffff00000081";
|
||||||
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_ACCOUNT_FILTER: &str =
|
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP: &str = "00000000-0000-0000-0000-ffff00000082";
|
||||||
"00000000-0000-0000-0000-ffff00000082";
|
|
||||||
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET: &str =
|
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET: &str =
|
||||||
"00000000-0000-0000-0000-ffff00000083";
|
"00000000-0000-0000-0000-ffff00000083";
|
||||||
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str =
|
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_TOKEN_KEY: &str = "00000000-0000-0000-0000-ffff00000084";
|
||||||
"00000000-0000-0000-0000-ffff00000084";
|
|
||||||
pub const STR_UUID_SCHEMA_CLASS_OAUTH2_RS: &str = "00000000-0000-0000-0000-ffff00000085";
|
pub const STR_UUID_SCHEMA_CLASS_OAUTH2_RS: &str = "00000000-0000-0000-0000-ffff00000085";
|
||||||
pub const STR_UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC: &str = "00000000-0000-0000-0000-ffff00000086";
|
pub const STR_UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC: &str = "00000000-0000-0000-0000-ffff00000086";
|
||||||
pub const STR_UUID_SCHEMA_ATTR_CN: &str = "00000000-0000-0000-0000-ffff00000087";
|
pub const STR_UUID_SCHEMA_ATTR_CN: &str = "00000000-0000-0000-0000-ffff00000087";
|
||||||
pub const STR_UUID_SCHEMA_ATTR_DOMAIN_TOKEN_KEY: &str = "00000000-0000-0000-0000-ffff00000088";
|
pub const STR_UUID_SCHEMA_ATTR_DOMAIN_TOKEN_KEY: &str = "00000000-0000-0000-0000-ffff00000088";
|
||||||
|
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_IMPLICIT_SCOPES: &str =
|
||||||
|
"00000000-0000-0000-0000-ffff00000089";
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -201,6 +201,7 @@ pub const STR_UUID_ANONYMOUS: &str = "00000000-0000-0000-0000-ffffffffffff";
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref UUID_ADMIN: Uuid = Uuid::parse_str(STR_UUID_ADMIN).unwrap();
|
pub static ref UUID_ADMIN: Uuid = Uuid::parse_str(STR_UUID_ADMIN).unwrap();
|
||||||
|
pub static ref UUID_SYSTEM_ADMINS: Uuid = Uuid::parse_str(STR_UUID_SYSTEM_ADMINS).unwrap();
|
||||||
pub static ref UUID_IDM_ADMIN: Uuid = Uuid::parse_str(STR_UUID_IDM_ADMIN_V1).unwrap();
|
pub static ref UUID_IDM_ADMIN: Uuid = Uuid::parse_str(STR_UUID_IDM_ADMIN_V1).unwrap();
|
||||||
pub static ref UUID_DOES_NOT_EXIST: Uuid = Uuid::parse_str(STR_UUID_DOES_NOT_EXIST).unwrap();
|
pub static ref UUID_DOES_NOT_EXIST: Uuid = Uuid::parse_str(STR_UUID_DOES_NOT_EXIST).unwrap();
|
||||||
pub static ref UUID_ANONYMOUS: Uuid = Uuid::parse_str(STR_UUID_ANONYMOUS).unwrap();
|
pub static ref UUID_ANONYMOUS: Uuid = Uuid::parse_str(STR_UUID_ANONYMOUS).unwrap();
|
||||||
|
|
|
@ -449,6 +449,11 @@ pub fn create_https_server(
|
||||||
.patch(oauth2_id_patch)
|
.patch(oauth2_id_patch)
|
||||||
.delete(oauth2_id_delete);
|
.delete(oauth2_id_delete);
|
||||||
|
|
||||||
|
oauth2_route
|
||||||
|
.at("/:id/_scopemap/:group")
|
||||||
|
.post(oauth2_id_scopemap_post)
|
||||||
|
.delete(oauth2_id_scopemap_delete);
|
||||||
|
|
||||||
let mut self_route = appserver.at("/v1/self");
|
let mut self_route = appserver.at("/v1/self");
|
||||||
self_route.at("/").get(whoami);
|
self_route.at("/").get(whoami);
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,40 @@ pub async fn oauth2_id_patch(mut req: tide::Request<AppState>) -> tide::Result {
|
||||||
to_tide_response(res, hvalue)
|
to_tide_response(res, hvalue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn oauth2_id_scopemap_post(mut req: tide::Request<AppState>) -> tide::Result {
|
||||||
|
let uat = req.get_current_uat();
|
||||||
|
let id = req.get_url_param("id")?;
|
||||||
|
let group = req.get_url_param("group")?;
|
||||||
|
|
||||||
|
let scopes: Vec<String> = req.body_json().await?;
|
||||||
|
|
||||||
|
let filter = oauth2_id(&id);
|
||||||
|
|
||||||
|
let (eventid, hvalue) = req.new_eventid();
|
||||||
|
let res = req
|
||||||
|
.state()
|
||||||
|
.qe_w_ref
|
||||||
|
.handle_oauth2_scopemap_create(uat, group, scopes, filter, eventid)
|
||||||
|
.await;
|
||||||
|
to_tide_response(res, hvalue)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn oauth2_id_scopemap_delete(req: tide::Request<AppState>) -> tide::Result {
|
||||||
|
let uat = req.get_current_uat();
|
||||||
|
let id = req.get_url_param("id")?;
|
||||||
|
let group = req.get_url_param("group")?;
|
||||||
|
|
||||||
|
let filter = oauth2_id(&id);
|
||||||
|
|
||||||
|
let (eventid, hvalue) = req.new_eventid();
|
||||||
|
let res = req
|
||||||
|
.state()
|
||||||
|
.qe_w_ref
|
||||||
|
.handle_oauth2_scopemap_delete(uat, group, filter, eventid)
|
||||||
|
.await;
|
||||||
|
to_tide_response(res, hvalue)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn oauth2_id_delete(req: tide::Request<AppState>) -> tide::Result {
|
pub async fn oauth2_id_delete(req: tide::Request<AppState>) -> tide::Result {
|
||||||
// Delete this
|
// Delete this
|
||||||
let uat = req.get_current_uat();
|
let uat = req.get_current_uat();
|
||||||
|
|
|
@ -1592,6 +1592,19 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
||||||
self.attrs.get(attr)
|
self.attrs.get(attr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn get_ava_as_oauthscopes(&self, attr: &str) -> Option<impl Iterator<Item = &str>> {
|
||||||
|
self.attrs.get(attr).and_then(|vs| vs.as_oauthscope_iter())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn get_ava_as_oauthscopemaps(
|
||||||
|
&self,
|
||||||
|
attr: &str,
|
||||||
|
) -> Option<&std::collections::BTreeMap<Uuid, std::collections::BTreeSet<String>>> {
|
||||||
|
self.attrs.get(attr).and_then(|vs| vs.as_oauthscopemap())
|
||||||
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
/// If possible, return an iterator over the set of values transformed into a `&str`.
|
/// If possible, return an iterator over the set of values transformed into a `&str`.
|
||||||
pub fn get_ava_as_str(&self, attr: &str) -> Option<impl Iterator<Item = &str>> {
|
pub fn get_ava_as_str(&self, attr: &str) -> Option<impl Iterator<Item = &str>> {
|
||||||
|
@ -1600,7 +1613,7 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
/// If possible, return an iterator over the set of values transformed into a `&Uuid`.
|
/// If possible, return an iterator over the set of values transformed into a `&Uuid`.
|
||||||
pub fn get_ava_as_refuuid(&self, attr: &str) -> Option<impl Iterator<Item = &Uuid>> {
|
pub fn get_ava_as_refuuid(&self, attr: &str) -> Option<Box<dyn Iterator<Item = &Uuid> + '_>> {
|
||||||
// If any value is NOT a reference, it's filtered out.
|
// If any value is NOT a reference, it's filtered out.
|
||||||
self.get_ava_set(attr).and_then(|vs| vs.as_ref_uuid_iter())
|
self.get_ava_set(attr).and_then(|vs| vs.as_ref_uuid_iter())
|
||||||
}
|
}
|
||||||
|
@ -1694,6 +1707,10 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
||||||
self.attrs.get(attr).and_then(|vs| vs.to_uuid_single())
|
self.attrs.get(attr).and_then(|vs| vs.to_uuid_single())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_ava_single_refer(&self, attr: &str) -> Option<&Uuid> {
|
||||||
|
self.attrs.get(attr).and_then(|vs| vs.to_refer_single())
|
||||||
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
/// Return a single protocol filter, if valid to transform this value.
|
/// Return a single protocol filter, if valid to transform this value.
|
||||||
pub fn get_ava_single_protofilter(&self, attr: &str) -> Option<&ProtoFilter> {
|
pub fn get_ava_single_protofilter(&self, attr: &str) -> Option<&ProtoFilter> {
|
||||||
|
@ -1860,9 +1877,11 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
||||||
// Get the schema attribute type out.
|
// Get the schema attribute type out.
|
||||||
match schema.is_multivalue(k) {
|
match schema.is_multivalue(k) {
|
||||||
Ok(r) => {
|
Ok(r) => {
|
||||||
if !r {
|
if !r || k == "systemmust" || k == "systemmay" {
|
||||||
// As this is single value, purge then present to maintain this
|
// As this is single value, purge then present to maintain this
|
||||||
// invariant
|
// invariant. The other situation we purge is within schema with
|
||||||
|
// the system types where we need to be able to express REMOVAL
|
||||||
|
// of attributes, thus we need the purge.
|
||||||
mods.push_mod(Modify::Purged(k.clone()));
|
mods.push_mod(Modify::Purged(k.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1907,21 +1926,30 @@ where
|
||||||
|
|
||||||
/// Remove an attribute-value pair from this entry.
|
/// Remove an attribute-value pair from this entry.
|
||||||
fn remove_ava(&mut self, attr: &str, value: &PartialValue) {
|
fn remove_ava(&mut self, attr: &str, value: &PartialValue) {
|
||||||
// It would be great to remove these extra allocations, but they
|
let rm = if let Some(vs) = self.attrs.get_mut(attr) {
|
||||||
// really don't cost much :(
|
vs.remove(value);
|
||||||
self.attrs.entry(AttrString::from(attr)).and_modify(|v| {
|
vs.is_empty()
|
||||||
// Here we need to actually do a check/binary search ...
|
} else {
|
||||||
v.remove(value);
|
false
|
||||||
});
|
};
|
||||||
|
//
|
||||||
|
if rm {
|
||||||
|
self.attrs.remove(attr);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need something that can remove by difference?
|
|
||||||
pub(crate) fn remove_avas(&mut self, attr: &str, values: &BTreeSet<PartialValue>) {
|
pub(crate) fn remove_avas(&mut self, attr: &str, values: &BTreeSet<PartialValue>) {
|
||||||
if let Some(vs) = self.attrs.get_mut(attr) {
|
let rm = if let Some(vs) = self.attrs.get_mut(attr) {
|
||||||
values.iter().for_each(|k| {
|
values.iter().for_each(|k| {
|
||||||
vs.remove(k);
|
vs.remove(k);
|
||||||
})
|
});
|
||||||
}
|
vs.is_empty()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if rm {
|
||||||
|
self.attrs.remove(attr);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove all values of this attribute from the entry.
|
/// Remove all values of this attribute from the entry.
|
||||||
|
@ -2235,13 +2263,13 @@ mod tests {
|
||||||
e.apply_modlist(&present_single_mods);
|
e.apply_modlist(&present_single_mods);
|
||||||
assert!(e.attribute_equality("attr", &PartialValue::new_iutf8("value")));
|
assert!(e.attribute_equality("attr", &PartialValue::new_iutf8("value")));
|
||||||
e.apply_modlist(&remove_mods);
|
e.apply_modlist(&remove_mods);
|
||||||
assert!(e.attrs.get("attr").unwrap().is_empty());
|
assert!(e.attrs.get("attr").is_none());
|
||||||
|
|
||||||
let remove_empty_mods = remove_mods;
|
let remove_empty_mods = remove_mods;
|
||||||
|
|
||||||
e.apply_modlist(&remove_empty_mods);
|
e.apply_modlist(&remove_empty_mods);
|
||||||
|
|
||||||
assert!(e.attrs.get("attr").unwrap().is_empty());
|
assert!(e.attrs.get("attr").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -151,4 +151,13 @@ impl Identity {
|
||||||
.attribute_equality("claim", &PartialValue::new_iutf8(claim)),
|
.attribute_equality("claim", &PartialValue::new_iutf8(claim)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_memberof(&self, group: Uuid) -> bool {
|
||||||
|
match &self.origin {
|
||||||
|
IdentType::Internal => false,
|
||||||
|
IdentType::User(u) => u
|
||||||
|
.entry
|
||||||
|
.attribute_equality("memberof", &PartialValue::new_refer(group)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,10 @@ use fernet::Fernet;
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use kanidm_proto::v1::UserAuthToken;
|
use kanidm_proto::v1::UserAuthToken;
|
||||||
use openssl::sha;
|
use openssl::sha;
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
use tracing::trace;
|
||||||
use url::{Origin, Url};
|
use url::{Origin, Url};
|
||||||
use webauthn_rs::base64_data::Base64UrlSafeData;
|
use webauthn_rs::base64_data::Base64UrlSafeData;
|
||||||
|
|
||||||
|
@ -76,6 +78,8 @@ struct ConsentToken {
|
||||||
pub code_challenge: Base64UrlSafeData,
|
pub code_challenge: Base64UrlSafeData,
|
||||||
// Where the RS wants us to go back to.
|
// Where the RS wants us to go back to.
|
||||||
pub redirect_uri: Url,
|
pub redirect_uri: Url,
|
||||||
|
// The scopes being granted
|
||||||
|
pub scopes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// consent token?
|
// consent token?
|
||||||
|
@ -89,6 +93,8 @@ struct TokenExchangeCode {
|
||||||
pub code_challenge: Base64UrlSafeData,
|
pub code_challenge: Base64UrlSafeData,
|
||||||
// The original redirect uri
|
// The original redirect uri
|
||||||
pub redirect_uri: Url,
|
pub redirect_uri: Url,
|
||||||
|
// The scopes being granted
|
||||||
|
pub scopes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// consentPermitResponse
|
// consentPermitResponse
|
||||||
|
@ -103,32 +109,43 @@ pub struct AuthorisePermitSuccess {
|
||||||
pub code: String,
|
pub code: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// The cache structure
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Oauth2RSTokenFormat {
|
||||||
|
Uat,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Oauth2RSBasic {
|
pub struct Oauth2RS {
|
||||||
name: String,
|
name: String,
|
||||||
|
displayname: String,
|
||||||
uuid: Uuid,
|
uuid: Uuid,
|
||||||
origin: Origin,
|
origin: Origin,
|
||||||
|
scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
|
||||||
|
implicit_scopes: Vec<String>,
|
||||||
|
// scope group map (hard)
|
||||||
|
// scope group map (soft)
|
||||||
|
// Client Auth Type (basic is all we support for now.
|
||||||
authz_secret: String,
|
authz_secret: String,
|
||||||
|
// Our internal exchange encryption material for this rs.
|
||||||
token_fernet: Fernet,
|
token_fernet: Fernet,
|
||||||
|
// What format we issue tokens as. By default prefer ANYTHING except jwt.
|
||||||
|
token_format: Oauth2RSTokenFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Oauth2RSBasic {
|
impl std::fmt::Debug for Oauth2RS {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
f.debug_struct("Oauth2RSBasic")
|
f.debug_struct("Oauth2RS")
|
||||||
.field("name", &self.name)
|
.field("name", &self.name)
|
||||||
|
.field("displayname", &self.displayname)
|
||||||
.field("uuid", &self.uuid)
|
.field("uuid", &self.uuid)
|
||||||
.field("origin", &self.origin)
|
.field("origin", &self.origin)
|
||||||
|
.field("scope_maps", &self.scope_maps)
|
||||||
|
.field("implicit_scopes", &self.implicit_scopes)
|
||||||
|
.field("token_format", &self.token_format)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Oauth2RS {
|
|
||||||
Basic(Oauth2RSBasic),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct Oauth2RSInner {
|
struct Oauth2RSInner {
|
||||||
fernet: Fernet,
|
fernet: Fernet,
|
||||||
|
@ -186,44 +203,68 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
||||||
let rs_set: Result<HashMap<_, _>, _> = value
|
let rs_set: Result<HashMap<_, _>, _> = value
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|ent| {
|
.map(|ent| {
|
||||||
|
let uuid = *ent.get_uuid();
|
||||||
|
admin_info!(?uuid, "Checking oauth2 configuration");
|
||||||
// From each entry, attempt to make an oauth2 configuration.
|
// From each entry, attempt to make an oauth2 configuration.
|
||||||
if !ent.attribute_equality("class", &CLASS_OAUTH2) {
|
if !ent.attribute_equality("class", &CLASS_OAUTH2) {
|
||||||
|
admin_error!("Missing class oauth2_resource_server");
|
||||||
// Check we have oauth2_resource_server class
|
// Check we have oauth2_resource_server class
|
||||||
Err(OperationError::InvalidEntryState)
|
Err(OperationError::InvalidEntryState)
|
||||||
} else if ent.attribute_equality("class", &CLASS_OAUTH2_BASIC) {
|
} else if ent.attribute_equality("class", &CLASS_OAUTH2_BASIC) {
|
||||||
// If we have oauth2_resource_server_basic
|
// If we have oauth2_resource_server_basic
|
||||||
// Now we know we can load the attrs.
|
// Now we know we can load the attrs.
|
||||||
let uuid = *ent.get_uuid();
|
trace!("name");
|
||||||
let name = ent
|
let name = ent
|
||||||
.get_ava_single_str("oauth2_rs_name")
|
.get_ava_single_str("oauth2_rs_name")
|
||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.ok_or(OperationError::InvalidValueState)?;
|
.ok_or(OperationError::InvalidValueState)?;
|
||||||
|
trace!("displayname");
|
||||||
|
let displayname = ent
|
||||||
|
.get_ava_single_str("displayname")
|
||||||
|
.map(str::to_string)
|
||||||
|
.ok_or(OperationError::InvalidValueState)?;
|
||||||
|
trace!("origin");
|
||||||
let origin = ent
|
let origin = ent
|
||||||
.get_ava_single_url("oauth2_rs_origin")
|
.get_ava_single_url("oauth2_rs_origin")
|
||||||
.map(|url| url.origin())
|
.map(|url| url.origin())
|
||||||
.ok_or(OperationError::InvalidValueState)?;
|
.ok_or(OperationError::InvalidValueState)?;
|
||||||
|
trace!("authz_secret");
|
||||||
let authz_secret = ent
|
let authz_secret = ent
|
||||||
.get_ava_single_str("oauth2_rs_basic_secret")
|
.get_ava_single_str("oauth2_rs_basic_secret")
|
||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.ok_or(OperationError::InvalidValueState)?;
|
.ok_or(OperationError::InvalidValueState)?;
|
||||||
|
trace!("token_key");
|
||||||
let token_fernet = ent
|
let token_fernet = ent
|
||||||
.get_ava_single_secret("oauth2_rs_basic_token_key")
|
.get_ava_single_secret("oauth2_rs_token_key")
|
||||||
.ok_or(OperationError::InvalidValueState)
|
.ok_or(OperationError::InvalidValueState)
|
||||||
.and_then(|key| {
|
.and_then(|key| {
|
||||||
Fernet::new(key).ok_or(OperationError::CryptographyError)
|
Fernet::new(key).ok_or(OperationError::CryptographyError)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Currently unsure if this is how I want to handle this.
|
trace!("scope_maps");
|
||||||
// let oauth2_rs_account_filter = ent.get_ava_single_protofilter("oauth2_rs_account_filter")
|
let scope_maps = ent
|
||||||
|
.get_ava_as_oauthscopemaps("oauth2_rs_scope_map")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| BTreeMap::new());
|
||||||
|
|
||||||
|
trace!("implicit_scopes");
|
||||||
|
let implicit_scopes = ent
|
||||||
|
.get_ava_as_oauthscopes("oauth2_rs_implicit_scopes")
|
||||||
|
.map(|iter| iter.map(str::to_string).collect())
|
||||||
|
.unwrap_or_else(|| Vec::new());
|
||||||
|
|
||||||
let client_id = name.clone();
|
let client_id = name.clone();
|
||||||
let rscfg = Oauth2RS::Basic(Oauth2RSBasic {
|
let rscfg = Oauth2RS {
|
||||||
name,
|
name,
|
||||||
|
displayname,
|
||||||
uuid,
|
uuid,
|
||||||
origin,
|
origin,
|
||||||
|
scope_maps,
|
||||||
|
implicit_scopes,
|
||||||
authz_secret,
|
authz_secret,
|
||||||
token_fernet,
|
token_fernet,
|
||||||
});
|
token_format: Oauth2RSTokenFormat::Uat,
|
||||||
|
};
|
||||||
|
|
||||||
Ok((client_id, rscfg))
|
Ok((client_id, rscfg))
|
||||||
} else {
|
} else {
|
||||||
|
@ -275,28 +316,59 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
Oauth2Error::InvalidRequest
|
Oauth2Error::InvalidRequest
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// scopes
|
// redirect_uri must be part of the client_id origin.
|
||||||
|
if auth_req.redirect_uri.origin() != o2rs.origin {
|
||||||
|
admin_warn!(
|
||||||
|
origin = ?o2rs.origin,
|
||||||
|
"Invalid oauth2 redirect_uri (must be related to origin of)"
|
||||||
|
);
|
||||||
|
return Err(Oauth2Error::InvalidRequest);
|
||||||
|
}
|
||||||
|
|
||||||
// user authorisation filter
|
// scopes - you need to have every requested scope or this req is denied.
|
||||||
|
let req_scopes: BTreeSet<_> = auth_req.scope.split_ascii_whitespace().collect();
|
||||||
|
if req_scopes.is_empty() {
|
||||||
|
admin_error!("Invalid oauth2 request - must contain at least one requested scope");
|
||||||
|
return Err(Oauth2Error::InvalidRequest);
|
||||||
|
}
|
||||||
|
let uat_scopes: BTreeSet<_> = o2rs
|
||||||
|
.implicit_scopes
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.chain(
|
||||||
|
o2rs.scope_maps
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(u, m)| {
|
||||||
|
if ident.is_memberof(*u) {
|
||||||
|
Some(m.iter().map(|s| s.as_str()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flatten(),
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Needs to use s.to_string due to &&str which can't use the str::to_string
|
||||||
|
let avail_scopes: Vec<String> = req_scopes
|
||||||
|
.intersection(&uat_scopes)
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if avail_scopes.len() != req_scopes.len() {
|
||||||
|
admin_warn!(
|
||||||
|
%ident,
|
||||||
|
%auth_req.scope,
|
||||||
|
"Identity does not have access to the requested scopes"
|
||||||
|
);
|
||||||
|
return Err(Oauth2Error::AccessDenied);
|
||||||
|
}
|
||||||
|
|
||||||
// Subseqent we then return an encrypted session handle which allows
|
// Subseqent we then return an encrypted session handle which allows
|
||||||
// the user to indicate their consent to this authorisation.
|
// the user to indicate their consent to this authorisation.
|
||||||
//
|
//
|
||||||
// This session handle is what we use in "permit" to generate the redirect.
|
// This session handle is what we use in "permit" to generate the redirect.
|
||||||
|
|
||||||
match o2rs {
|
|
||||||
Oauth2RS::Basic(rsbasic) => {
|
|
||||||
// redirect_uri must be part of the client_id origin.
|
|
||||||
if auth_req.redirect_uri.origin() != rsbasic.origin {
|
|
||||||
admin_warn!(
|
|
||||||
origin = ?rsbasic.origin,
|
|
||||||
"Invalid oauth2 redirect_uri (must be related to origin of)"
|
|
||||||
);
|
|
||||||
return Err(Oauth2Error::InvalidRequest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let consent_req = ConsentToken {
|
let consent_req = ConsentToken {
|
||||||
client_id: auth_req.client_id.clone(),
|
client_id: auth_req.client_id.clone(),
|
||||||
ident_id: ident.get_event_origin_id(),
|
ident_id: ident.get_event_origin_id(),
|
||||||
|
@ -304,6 +376,7 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
state: auth_req.state.clone(),
|
state: auth_req.state.clone(),
|
||||||
code_challenge: auth_req.code_challenge.clone(),
|
code_challenge: auth_req.code_challenge.clone(),
|
||||||
redirect_uri: auth_req.redirect_uri.clone(),
|
redirect_uri: auth_req.redirect_uri.clone(),
|
||||||
|
scopes: avail_scopes.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let consent_data = serde_json::to_vec(&consent_req).map_err(|e| {
|
let consent_data = serde_json::to_vec(&consent_req).map_err(|e| {
|
||||||
|
@ -317,8 +390,8 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
.encrypt_at_time(&consent_data, ct.as_secs());
|
.encrypt_at_time(&consent_data, ct.as_secs());
|
||||||
|
|
||||||
Ok(ConsentRequest {
|
Ok(ConsentRequest {
|
||||||
client_name: auth_req.client_id.clone(),
|
client_name: o2rs.displayname.clone(),
|
||||||
scopes: Vec::new(),
|
scopes: avail_scopes,
|
||||||
consent_token,
|
consent_token,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -359,13 +432,14 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the resource server config based on this client_id.
|
// Get the resource server config based on this client_id.
|
||||||
let o2rs_fernet = match self.inner.rs_set.get(&consent_req.client_id) {
|
let o2rs = self
|
||||||
Some(Oauth2RS::Basic(rsbasic)) => &rsbasic.token_fernet,
|
.inner
|
||||||
None => {
|
.rs_set
|
||||||
|
.get(&consent_req.client_id)
|
||||||
|
.ok_or_else(|| {
|
||||||
admin_error!("Invalid consent request oauth2 client_id");
|
admin_error!("Invalid consent request oauth2 client_id");
|
||||||
return Err(OperationError::InvalidRequestState);
|
OperationError::InvalidRequestState
|
||||||
}
|
})?;
|
||||||
};
|
|
||||||
|
|
||||||
// Extract the state, code challenge, redirect_uri
|
// Extract the state, code challenge, redirect_uri
|
||||||
|
|
||||||
|
@ -373,6 +447,7 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
uat: uat.clone(),
|
uat: uat.clone(),
|
||||||
code_challenge: consent_req.code_challenge,
|
code_challenge: consent_req.code_challenge,
|
||||||
redirect_uri: consent_req.redirect_uri.clone(),
|
redirect_uri: consent_req.redirect_uri.clone(),
|
||||||
|
scopes: consent_req.scopes,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Encrypt the exchange token with the fernet key of the client resource server
|
// Encrypt the exchange token with the fernet key of the client resource server
|
||||||
|
@ -381,7 +456,7 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
OperationError::SerdeJsonError
|
OperationError::SerdeJsonError
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let code = o2rs_fernet.encrypt_at_time(&code_data, ct.as_secs());
|
let code = o2rs.token_fernet.encrypt_at_time(&code_data, ct.as_secs());
|
||||||
|
|
||||||
Ok(AuthorisePermitSuccess {
|
Ok(AuthorisePermitSuccess {
|
||||||
redirect_uri: consent_req.redirect_uri,
|
redirect_uri: consent_req.redirect_uri,
|
||||||
|
@ -434,21 +509,17 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// check the secret.
|
// check the secret.
|
||||||
let o2rs_fernet = match o2rs {
|
if o2rs.authz_secret != secret {
|
||||||
Oauth2RS::Basic(rsbasic) => {
|
security_info!("Invalid oauth2 client_id secret");
|
||||||
if rsbasic.authz_secret != secret {
|
return Err(Oauth2Error::AuthenticationRequired);
|
||||||
security_info!("Invalid oauth2 client_id secret");
|
}
|
||||||
return Err(Oauth2Error::AuthenticationRequired);
|
// We are authenticated! Yay! Now we can actually check things ...
|
||||||
}
|
|
||||||
// We are authenticated! Yay! Now we can actually check things ...
|
|
||||||
&rsbasic.token_fernet
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check the token_req is within the valid time, and correctly signed for
|
// Check the token_req is within the valid time, and correctly signed for
|
||||||
// this client.
|
// this client.
|
||||||
|
|
||||||
let code_xchg: TokenExchangeCode = o2rs_fernet
|
let code_xchg: TokenExchangeCode = o2rs
|
||||||
|
.token_fernet
|
||||||
.decrypt_at_time(&token_req.code, Some(60), ct.as_secs())
|
.decrypt_at_time(&token_req.code, Some(60), ct.as_secs())
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
admin_error!("Failed to decrypt token exchange request");
|
admin_error!("Failed to decrypt token exchange request");
|
||||||
|
@ -491,19 +562,30 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
return Err(Oauth2Error::AccessDenied);
|
return Err(Oauth2Error::AccessDenied);
|
||||||
};
|
};
|
||||||
|
|
||||||
let access_token = serde_json::to_vec(&code_xchg.uat)
|
let scope = if code_xchg.scopes.is_empty() {
|
||||||
.map_err(|e| {
|
None
|
||||||
admin_error!(err = ?e, "Unable to encode uat data");
|
} else {
|
||||||
Oauth2Error::ServerError(OperationError::SerdeJsonError)
|
Some(code_xchg.scopes.join(" "))
|
||||||
})
|
};
|
||||||
.map(|data| o2rs_fernet.encrypt_at_time(&data, ct.as_secs()))?;
|
|
||||||
|
// If we are type == Uat, then we re-use the same encryption material here.
|
||||||
|
let access_token_data = serde_json::to_vec(&code_xchg.uat).map_err(|e| {
|
||||||
|
admin_error!(err = ?e, "Unable to encode uat data");
|
||||||
|
Oauth2Error::ServerError(OperationError::SerdeJsonError)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let access_token = match o2rs.token_format {
|
||||||
|
Oauth2RSTokenFormat::Uat => o2rs
|
||||||
|
.token_fernet
|
||||||
|
.encrypt_at_time(&access_token_data, ct.as_secs()),
|
||||||
|
};
|
||||||
|
|
||||||
Ok(AccessTokenResponse {
|
Ok(AccessTokenResponse {
|
||||||
access_token,
|
access_token,
|
||||||
token_type: "bearer".to_string(),
|
token_type: "bearer".to_string(),
|
||||||
expires_in,
|
expires_in,
|
||||||
refresh_token: None,
|
refresh_token: None,
|
||||||
scope: None,
|
scope,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -552,7 +634,7 @@ mod tests {
|
||||||
code_challenge: Base64UrlSafeData($code_challenge),
|
code_challenge: Base64UrlSafeData($code_challenge),
|
||||||
code_challenge_method: CodeChallengeMethod::S256,
|
code_challenge_method: CodeChallengeMethod::S256,
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||||
scope: "".to_string(),
|
scope: "test".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
$idms_prox_read
|
$idms_prox_read
|
||||||
|
@ -576,9 +658,16 @@ mod tests {
|
||||||
("class", Value::new_class("oauth2_resource_server_basic")),
|
("class", Value::new_class("oauth2_resource_server_basic")),
|
||||||
("uuid", Value::new_uuid(uuid)),
|
("uuid", Value::new_uuid(uuid)),
|
||||||
("oauth2_rs_name", Value::new_iname("test_resource_server")),
|
("oauth2_rs_name", Value::new_iname("test_resource_server")),
|
||||||
|
("displayname", Value::new_utf8s("test_resource_server")),
|
||||||
(
|
(
|
||||||
"oauth2_rs_origin",
|
"oauth2_rs_origin",
|
||||||
Value::new_url_s("https://demo.example.com").unwrap()
|
Value::new_url_s("https://demo.example.com").unwrap()
|
||||||
|
),
|
||||||
|
("oauth2_rs_implicit_scopes", Value::new_oauthscope("test")),
|
||||||
|
// System admins
|
||||||
|
(
|
||||||
|
"oauth2_rs_scope_map",
|
||||||
|
Value::new_oauthscopemap(*UUID_SYSTEM_ADMINS, btreeset!["read".to_string()])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let ce = CreateEvent::new_internal(vec![e]);
|
let ce = CreateEvent::new_internal(vec![e]);
|
||||||
|
@ -610,6 +699,24 @@ mod tests {
|
||||||
(secret, uat, ident)
|
(secret, uat, ident)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn setup_anon(idms: &IdmServer, ct: Duration) -> (UserAuthToken, Identity) {
|
||||||
|
let mut idms_prox_write = idms.proxy_write(ct);
|
||||||
|
let account = idms_prox_write
|
||||||
|
.target_to_account(&UUID_IDM_ADMIN)
|
||||||
|
.expect("account must exist");
|
||||||
|
let session_id = uuid::Uuid::new_v4();
|
||||||
|
let uat = account
|
||||||
|
.to_userauthtoken(session_id, ct, AuthType::Anonymous)
|
||||||
|
.expect("Unable to create uat");
|
||||||
|
let ident = idms_prox_write
|
||||||
|
.process_uat_to_identity(&uat, ct)
|
||||||
|
.expect("Unable to process uat");
|
||||||
|
|
||||||
|
idms_prox_write.commit().expect("failed to commit");
|
||||||
|
|
||||||
|
(uat, ident)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_idm_oauth2_basic_function() {
|
fn test_idm_oauth2_basic_function() {
|
||||||
run_idm_test!(|_qs: &QueryServer,
|
run_idm_test!(|_qs: &QueryServer,
|
||||||
|
@ -666,6 +773,10 @@ mod tests {
|
||||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||||
let (_secret, uat, ident) = setup_oauth2_resource_server(idms, ct);
|
let (_secret, uat, ident) = setup_oauth2_resource_server(idms, ct);
|
||||||
|
|
||||||
|
let (anon_uat, anon_ident) = setup_anon(idms, ct);
|
||||||
|
|
||||||
|
// Need a uat from a user not in the group. Probs anonymous.
|
||||||
|
|
||||||
let idms_prox_read = idms.proxy_read();
|
let idms_prox_read = idms.proxy_read();
|
||||||
|
|
||||||
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
|
@ -678,7 +789,7 @@ mod tests {
|
||||||
code_challenge: Base64UrlSafeData(code_challenge.clone()),
|
code_challenge: Base64UrlSafeData(code_challenge.clone()),
|
||||||
code_challenge_method: CodeChallengeMethod::S256,
|
code_challenge_method: CodeChallengeMethod::S256,
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||||
scope: "".to_string(),
|
scope: "test".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -696,7 +807,7 @@ mod tests {
|
||||||
code_challenge: Base64UrlSafeData(code_challenge.clone()),
|
code_challenge: Base64UrlSafeData(code_challenge.clone()),
|
||||||
code_challenge_method: CodeChallengeMethod::S256,
|
code_challenge_method: CodeChallengeMethod::S256,
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||||
scope: "".to_string(),
|
scope: "test".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -711,10 +822,10 @@ mod tests {
|
||||||
response_type: "code".to_string(),
|
response_type: "code".to_string(),
|
||||||
client_id: "test_resource_server".to_string(),
|
client_id: "test_resource_server".to_string(),
|
||||||
state: Base64UrlSafeData(vec![1, 2, 3]),
|
state: Base64UrlSafeData(vec![1, 2, 3]),
|
||||||
code_challenge: Base64UrlSafeData(code_challenge),
|
code_challenge: Base64UrlSafeData(code_challenge.clone()),
|
||||||
code_challenge_method: CodeChallengeMethod::S256,
|
code_challenge_method: CodeChallengeMethod::S256,
|
||||||
redirect_uri: Url::parse("https://totes.not.sus.org/oauth2/result").unwrap(),
|
redirect_uri: Url::parse("https://totes.not.sus.org/oauth2/result").unwrap(),
|
||||||
scope: "".to_string(),
|
scope: "test".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -723,6 +834,43 @@ mod tests {
|
||||||
.unwrap_err()
|
.unwrap_err()
|
||||||
== Oauth2Error::InvalidRequest
|
== Oauth2Error::InvalidRequest
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Requested scope is not available
|
||||||
|
let auth_req = AuthorisationRequest {
|
||||||
|
response_type: "code".to_string(),
|
||||||
|
client_id: "test_resource_server".to_string(),
|
||||||
|
state: Base64UrlSafeData(vec![1, 2, 3]),
|
||||||
|
code_challenge: Base64UrlSafeData(code_challenge.clone()),
|
||||||
|
code_challenge_method: CodeChallengeMethod::S256,
|
||||||
|
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||||
|
scope: "invalid_scope read".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
idms_prox_read
|
||||||
|
.check_oauth2_authorisation(&ident, &uat, &auth_req, ct)
|
||||||
|
.unwrap_err()
|
||||||
|
== Oauth2Error::AccessDenied
|
||||||
|
);
|
||||||
|
|
||||||
|
// Not a member of the group.
|
||||||
|
|
||||||
|
let auth_req = AuthorisationRequest {
|
||||||
|
response_type: "code".to_string(),
|
||||||
|
client_id: "test_resource_server".to_string(),
|
||||||
|
state: Base64UrlSafeData(vec![1, 2, 3]),
|
||||||
|
code_challenge: Base64UrlSafeData(code_challenge),
|
||||||
|
code_challenge_method: CodeChallengeMethod::S256,
|
||||||
|
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||||
|
scope: "read test".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
idms_prox_read
|
||||||
|
.check_oauth2_authorisation(&anon_ident, &anon_uat, &auth_req, ct)
|
||||||
|
.unwrap_err()
|
||||||
|
== Oauth2Error::AccessDenied
|
||||||
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1155,7 +1155,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: tracing
|
|
||||||
pub(crate) fn target_to_account(&mut self, target: &Uuid) -> Result<Account, OperationError> {
|
pub(crate) fn target_to_account(&mut self, target: &Uuid) -> Result<Account, OperationError> {
|
||||||
// Get the account
|
// Get the account
|
||||||
let account = self
|
let account = self
|
||||||
|
@ -1172,24 +1171,19 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
|
|
||||||
// Deny the change if the account is anonymous!
|
// Deny the change if the account is anonymous!
|
||||||
if account.is_anonymous() {
|
if account.is_anonymous() {
|
||||||
|
admin_warn!("Unable to convert anonymous to account during write txn");
|
||||||
Err(OperationError::SystemProtectedObject)
|
Err(OperationError::SystemProtectedObject)
|
||||||
} else {
|
} else {
|
||||||
Ok(account)
|
Ok(account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: tracing
|
|
||||||
pub fn set_account_password(
|
pub fn set_account_password(
|
||||||
&mut self,
|
&mut self,
|
||||||
pce: &PasswordChangeEvent,
|
pce: &PasswordChangeEvent,
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
let account = self.target_to_account(&pce.target)?;
|
let account = self.target_to_account(&pce.target)?;
|
||||||
|
|
||||||
// Deny the change if the account is anonymous!
|
|
||||||
if account.is_anonymous() {
|
|
||||||
return Err(OperationError::SystemProtectedObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the modifications we *want* to perform.
|
// Get the modifications we *want* to perform.
|
||||||
let modlist = account
|
let modlist = account
|
||||||
.gen_password_mod(pce.cleartext.as_str(), self.crypto_policy)
|
.gen_password_mod(pce.cleartext.as_str(), self.crypto_policy)
|
||||||
|
@ -1257,7 +1251,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: tracing
|
|
||||||
pub fn set_unix_account_password(
|
pub fn set_unix_account_password(
|
||||||
&mut self,
|
&mut self,
|
||||||
pce: &UnixPasswordChangeEvent,
|
pce: &UnixPasswordChangeEvent,
|
||||||
|
|
|
@ -100,6 +100,10 @@ impl ModifyList<ModifyInvalid> {
|
||||||
Self::new_list(vec![Modify::Present(AttrString::from(attr), v)])
|
Self::new_list(vec![Modify::Present(AttrString::from(attr), v)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_remove(attr: &str, pv: PartialValue) -> Self {
|
||||||
|
Self::new_list(vec![Modify::Removed(AttrString::from(attr), pv)])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_purge(attr: &str) -> Self {
|
pub fn new_purge(attr: &str) -> Self {
|
||||||
Self::new_list(vec![m_purge(attr)])
|
Self::new_list(vec![m_purge(attr)])
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,11 @@ macro_rules! oauth2_transform {
|
||||||
let v = Value::new_utf8(password_from_random());
|
let v = Value::new_utf8(password_from_random());
|
||||||
$e.add_ava("oauth2_rs_basic_secret", v);
|
$e.add_ava("oauth2_rs_basic_secret", v);
|
||||||
}
|
}
|
||||||
if !$e.attribute_pres("oauth2_rs_basic_token_key") {
|
if !$e.attribute_pres("oauth2_rs_token_key") {
|
||||||
security_info!("regenerating oauth2 token key");
|
security_info!("regenerating oauth2 token key");
|
||||||
let k = fernet::Fernet::generate_key();
|
let k = fernet::Fernet::generate_key();
|
||||||
let v = Value::new_secret_str(&k);
|
let v = Value::new_secret_str(&k);
|
||||||
$e.add_ava("oauth2_rs_basic_token_key", v);
|
$e.add_ava("oauth2_rs_token_key", v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -68,11 +68,13 @@ mod tests {
|
||||||
("class", Value::new_class("oauth2_resource_server")),
|
("class", Value::new_class("oauth2_resource_server")),
|
||||||
("class", Value::new_class("oauth2_resource_server_basic")),
|
("class", Value::new_class("oauth2_resource_server_basic")),
|
||||||
("uuid", Value::new_uuid(uuid)),
|
("uuid", Value::new_uuid(uuid)),
|
||||||
|
("displayname", Value::new_utf8s("test_resource_server")),
|
||||||
("oauth2_rs_name", Value::new_iname("test_resource_server")),
|
("oauth2_rs_name", Value::new_iname("test_resource_server")),
|
||||||
(
|
(
|
||||||
"oauth2_rs_origin",
|
"oauth2_rs_origin",
|
||||||
Value::new_url_s("https://demo.example.com").unwrap()
|
Value::new_url_s("https://demo.example.com").unwrap()
|
||||||
)
|
),
|
||||||
|
("oauth2_rs_implicit_scopes", Value::new_oauthscope("read"))
|
||||||
);
|
);
|
||||||
|
|
||||||
let create = vec![e];
|
let create = vec![e];
|
||||||
|
@ -87,7 +89,7 @@ mod tests {
|
||||||
.internal_search_uuid(&uuid)
|
.internal_search_uuid(&uuid)
|
||||||
.expect("failed to get oauth2 config");
|
.expect("failed to get oauth2 config");
|
||||||
assert!(e.attribute_pres("oauth2_rs_basic_secret"));
|
assert!(e.attribute_pres("oauth2_rs_basic_secret"));
|
||||||
assert!(e.attribute_pres("oauth2_rs_basic_token_key"));
|
assert!(e.attribute_pres("oauth2_rs_token_key"));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -102,12 +104,14 @@ mod tests {
|
||||||
("class", Value::new_class("oauth2_resource_server_basic")),
|
("class", Value::new_class("oauth2_resource_server_basic")),
|
||||||
("uuid", Value::new_uuid(uuid)),
|
("uuid", Value::new_uuid(uuid)),
|
||||||
("oauth2_rs_name", Value::new_iname("test_resource_server")),
|
("oauth2_rs_name", Value::new_iname("test_resource_server")),
|
||||||
|
("displayname", Value::new_utf8s("test_resource_server")),
|
||||||
(
|
(
|
||||||
"oauth2_rs_origin",
|
"oauth2_rs_origin",
|
||||||
Value::new_url_s("https://demo.example.com").unwrap()
|
Value::new_url_s("https://demo.example.com").unwrap()
|
||||||
),
|
),
|
||||||
|
("oauth2_rs_implicit_scopes", Value::new_oauthscope("read")),
|
||||||
("oauth2_rs_basic_secret", Value::new_utf8s("12345")),
|
("oauth2_rs_basic_secret", Value::new_utf8s("12345")),
|
||||||
("oauth2_rs_basic_token_key", Value::new_secret_str("12345"))
|
("oauth2_rs_token_key", Value::new_secret_str("12345"))
|
||||||
);
|
);
|
||||||
|
|
||||||
let preload = vec![e];
|
let preload = vec![e];
|
||||||
|
@ -118,7 +122,7 @@ mod tests {
|
||||||
filter!(f_eq("uuid", PartialValue::new_uuid(uuid))),
|
filter!(f_eq("uuid", PartialValue::new_uuid(uuid))),
|
||||||
ModifyList::new_list(vec![
|
ModifyList::new_list(vec![
|
||||||
Modify::Purged(AttrString::from("oauth2_rs_basic_secret"),),
|
Modify::Purged(AttrString::from("oauth2_rs_basic_secret"),),
|
||||||
Modify::Purged(AttrString::from("oauth2_rs_basic_token_key"),)
|
Modify::Purged(AttrString::from("oauth2_rs_token_key"),)
|
||||||
]),
|
]),
|
||||||
None,
|
None,
|
||||||
|qs: &QueryServerWriteTransaction| {
|
|qs: &QueryServerWriteTransaction| {
|
||||||
|
@ -126,10 +130,10 @@ mod tests {
|
||||||
.internal_search_uuid(&uuid)
|
.internal_search_uuid(&uuid)
|
||||||
.expect("failed to get oauth2 config");
|
.expect("failed to get oauth2 config");
|
||||||
assert!(e.attribute_pres("oauth2_rs_basic_secret"));
|
assert!(e.attribute_pres("oauth2_rs_basic_secret"));
|
||||||
assert!(e.attribute_pres("oauth2_rs_basic_token_key"));
|
assert!(e.attribute_pres("oauth2_rs_token_key"));
|
||||||
// Check the values are different.
|
// Check the values are different.
|
||||||
assert!(e.get_ava_single_str("oauth2_rs_basic_secret") != Some("12345"));
|
assert!(e.get_ava_single_str("oauth2_rs_basic_secret") != Some("12345"));
|
||||||
assert!(e.get_ava_single_secret("oauth2_rs_basic_token_key") != Some("12345"));
|
assert!(e.get_ava_single_secret("oauth2_rs_token_key") != Some("12345"));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ use crate::modify::Modify;
|
||||||
use crate::schema::SchemaTransaction;
|
use crate::schema::SchemaTransaction;
|
||||||
use kanidm_proto::v1::{ConsistencyError, PluginError};
|
use kanidm_proto::v1::{ConsistencyError, PluginError};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
// NOTE: This *must* be after base.rs!!!
|
// NOTE: This *must* be after base.rs!!!
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ impl ReferentialIntegrity {
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
if inner.is_empty() {
|
if inner.is_empty() {
|
||||||
// There is nothing to check! Move on.
|
// There is nothing to check! Move on.
|
||||||
admin_info!("no reference types modified, skipping check");
|
trace!("no reference types modified, skipping check");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +110,7 @@ impl Plugin for ReferentialIntegrity {
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
admin_error!("reference value could not convert to reference uuid.");
|
admin_error!(?vs, "reference value could not convert to reference uuid.");
|
||||||
admin_error!("If you are sure the name/uuid/spn exist, and that this is in error, you should run a verify task.");
|
admin_error!("If you are sure the name/uuid/spn exist, and that this is in error, you should run a verify task.");
|
||||||
Err(OperationError::InvalidAttribute(
|
Err(OperationError::InvalidAttribute(
|
||||||
"uuid could not become reference value".to_string(),
|
"uuid could not become reference value".to_string(),
|
||||||
|
@ -144,7 +145,7 @@ impl Plugin for ReferentialIntegrity {
|
||||||
v.to_ref_uuid()
|
v.to_ref_uuid()
|
||||||
.map(|uuid| PartialValue::new_uuid(*uuid))
|
.map(|uuid| PartialValue::new_uuid(*uuid))
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
admin_error!("reference value could not convert to reference uuid.");
|
admin_error!(?v, "reference value could not convert to reference uuid.");
|
||||||
admin_error!("If you are sure the name/uuid/spn exist, and that this is in error, you should run a verify task.");
|
admin_error!("If you are sure the name/uuid/spn exist, and that this is in error, you should run a verify task.");
|
||||||
OperationError::InvalidAttribute(
|
OperationError::InvalidAttribute(
|
||||||
"uuid could not become reference value".to_string(),
|
"uuid could not become reference value".to_string(),
|
||||||
|
@ -698,4 +699,61 @@ mod tests {
|
||||||
|_qs: &QueryServerWriteTransaction| {}
|
|_qs: &QueryServerWriteTransaction| {}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_remove_reference_oauth2() {
|
||||||
|
// Oauth2 types are also capable of uuid referencing to groups for their
|
||||||
|
// scope maps, so we need to check that when the group is deleted, that the
|
||||||
|
// scope map is also appropriately affected.
|
||||||
|
let ea: Entry<EntryInit, EntryNew> = entry_init!(
|
||||||
|
("class", Value::new_class("object")),
|
||||||
|
("class", Value::new_class("oauth2_resource_server")),
|
||||||
|
("class", Value::new_class("oauth2_resource_server_basic")),
|
||||||
|
("oauth2_rs_name", Value::new_iname("test_resource_server")),
|
||||||
|
("displayname", Value::new_utf8s("test_resource_server")),
|
||||||
|
(
|
||||||
|
"oauth2_rs_origin",
|
||||||
|
Value::new_url_s("https://demo.example.com").unwrap()
|
||||||
|
),
|
||||||
|
("oauth2_rs_implicit_scopes", Value::new_oauthscope("test")),
|
||||||
|
(
|
||||||
|
"oauth2_rs_scope_map",
|
||||||
|
Value::new_oauthscopemap(
|
||||||
|
Uuid::parse_str("cc8e95b4-c24f-4d68-ba54-8bed76f63930").expect("uuid"),
|
||||||
|
btreeset!["read".to_string()]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let eb: Entry<EntryInit, EntryNew> = entry_init!(
|
||||||
|
("class", Value::new_class("group")),
|
||||||
|
("name", Value::new_iname("testgroup")),
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
Value::new_uuids("cc8e95b4-c24f-4d68-ba54-8bed76f63930").expect("uuid")
|
||||||
|
),
|
||||||
|
("description", Value::new_utf8s("testgroup"))
|
||||||
|
);
|
||||||
|
|
||||||
|
let preload = vec![ea, eb];
|
||||||
|
|
||||||
|
run_delete_test!(
|
||||||
|
Ok(()),
|
||||||
|
preload,
|
||||||
|
filter!(f_eq("name", PartialValue::new_iname("testgroup"))),
|
||||||
|
None,
|
||||||
|
|qs: &QueryServerWriteTransaction| {
|
||||||
|
let cands = qs
|
||||||
|
.internal_search(filter!(f_eq(
|
||||||
|
"oauth2_rs_name",
|
||||||
|
PartialValue::new_iname("test_resource_server")
|
||||||
|
)))
|
||||||
|
.expect("Internal search failure");
|
||||||
|
let ue = cands.first().expect("No entry");
|
||||||
|
assert!(ue
|
||||||
|
.get_ava_as_oauthscopemaps("oauth2_rs_scope_map")
|
||||||
|
.is_none())
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -196,10 +196,13 @@ impl SchemaAttribute {
|
||||||
SyntaxType::DateTime => v.is_datetime(),
|
SyntaxType::DateTime => v.is_datetime(),
|
||||||
SyntaxType::EmailAddress => v.is_email_address(),
|
SyntaxType::EmailAddress => v.is_email_address(),
|
||||||
SyntaxType::Url => v.is_url(),
|
SyntaxType::Url => v.is_url(),
|
||||||
|
SyntaxType::OauthScope => v.is_oauthscope(),
|
||||||
|
SyntaxType::OauthScopeMap => v.is_oauthscopemap() || v.is_refer(),
|
||||||
};
|
};
|
||||||
if r {
|
if r {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
|
trace!(?a, ?self, ?v, "validate_pv InvalidAttributeSyntax");
|
||||||
Err(SchemaError::InvalidAttributeSyntax(a.to_string()))
|
Err(SchemaError::InvalidAttributeSyntax(a.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,6 +216,12 @@ impl SchemaAttribute {
|
||||||
let pv: &PartialValue = v.borrow();
|
let pv: &PartialValue = v.borrow();
|
||||||
self.validate_partialvalue(a, pv)
|
self.validate_partialvalue(a, pv)
|
||||||
} else {
|
} else {
|
||||||
|
trace!(
|
||||||
|
?a,
|
||||||
|
?self,
|
||||||
|
?v,
|
||||||
|
"value validation failure - InvalidAttributeSyntax"
|
||||||
|
);
|
||||||
Err(SchemaError::InvalidAttributeSyntax(a.to_string()))
|
Err(SchemaError::InvalidAttributeSyntax(a.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -222,6 +231,7 @@ impl SchemaAttribute {
|
||||||
// Check multivalue
|
// Check multivalue
|
||||||
if !self.multivalue && ava.len() > 1 {
|
if !self.multivalue && ava.len() > 1 {
|
||||||
// lrequest_error!("Ava len > 1 on single value attribute!");
|
// lrequest_error!("Ava len > 1 on single value attribute!");
|
||||||
|
admin_error!("Ava len > 1 on single value attribute!");
|
||||||
return Err(SchemaError::InvalidAttributeSyntax(a.to_string()));
|
return Err(SchemaError::InvalidAttributeSyntax(a.to_string()));
|
||||||
};
|
};
|
||||||
// If syntax, check the type is correct
|
// If syntax, check the type is correct
|
||||||
|
@ -245,11 +255,13 @@ impl SchemaAttribute {
|
||||||
SyntaxType::DateTime => ava.is_datetime(),
|
SyntaxType::DateTime => ava.is_datetime(),
|
||||||
SyntaxType::EmailAddress => ava.is_email_address(),
|
SyntaxType::EmailAddress => ava.is_email_address(),
|
||||||
SyntaxType::Url => ava.is_url(),
|
SyntaxType::Url => ava.is_url(),
|
||||||
|
SyntaxType::OauthScope => ava.is_oauthscope(),
|
||||||
|
SyntaxType::OauthScopeMap => ava.is_oauthscopemap(),
|
||||||
};
|
};
|
||||||
if valid {
|
if valid {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
trace!(?a, "InvalidAttributeSyntax");
|
admin_error!(?a, "validate_ava - InvalidAttributeSyntax");
|
||||||
Err(SchemaError::InvalidAttributeSyntax(a.to_string()))
|
Err(SchemaError::InvalidAttributeSyntax(a.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -466,7 +478,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
||||||
// No, they'll over-write each other ... but we do need name uniqueness.
|
// No, they'll over-write each other ... but we do need name uniqueness.
|
||||||
attributetypes.into_iter().for_each(|a| {
|
attributetypes.into_iter().for_each(|a| {
|
||||||
// Update the unique and ref caches.
|
// Update the unique and ref caches.
|
||||||
if a.syntax == SyntaxType::REFERENCE_UUID {
|
if a.syntax == SyntaxType::REFERENCE_UUID || a.syntax == SyntaxType::OauthScopeMap {
|
||||||
self.ref_cache.insert(a.name.clone(), a.clone());
|
self.ref_cache.insert(a.name.clone(), a.clone());
|
||||||
}
|
}
|
||||||
if a.unique {
|
if a.unique {
|
||||||
|
@ -1837,6 +1849,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_schema_entries() {
|
fn test_schema_entries() {
|
||||||
|
let _ = crate::tracing_tree::test_init();
|
||||||
// Given an entry, assert it's schema is valid
|
// Given an entry, assert it's schema is valid
|
||||||
// We do
|
// We do
|
||||||
let schema_outer = Schema::new().expect("failed to create schema");
|
let schema_outer = Schema::new().expect("failed to create schema");
|
||||||
|
|
|
@ -513,6 +513,8 @@ pub trait QueryServerTransaction<'a> {
|
||||||
SyntaxType::EmailAddress => Ok(Value::new_email_address_s(value)),
|
SyntaxType::EmailAddress => Ok(Value::new_email_address_s(value)),
|
||||||
SyntaxType::Url => Value::new_url_s(value)
|
SyntaxType::Url => Value::new_url_s(value)
|
||||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid Url (whatwg/url) syntax".to_string())),
|
.ok_or_else(|| OperationError::InvalidAttribute("Invalid Url (whatwg/url) syntax".to_string())),
|
||||||
|
SyntaxType::OauthScope => Ok(Value::new_oauthscope(value)),
|
||||||
|
SyntaxType::OauthScopeMap => Err(OperationError::InvalidAttribute("Oauth Scope Maps can not be supplied through modification - please use the IDM api".to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
@ -589,6 +591,24 @@ pub trait QueryServerTransaction<'a> {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
SyntaxType::OauthScopeMap => {
|
||||||
|
// See comments above.
|
||||||
|
PartialValue::new_oauthscopemap_s(value)
|
||||||
|
.or_else(|| {
|
||||||
|
let un = self
|
||||||
|
.name_to_uuid(value)
|
||||||
|
.unwrap_or_else(|_| *UUID_DOES_NOT_EXIST);
|
||||||
|
Some(PartialValue::new_oauthscopemap(un))
|
||||||
|
})
|
||||||
|
// I think this is unreachable due to how the .or_else works.
|
||||||
|
// See above case for how to avoid having unreachable code
|
||||||
|
.ok_or_else(|| {
|
||||||
|
OperationError::InvalidAttribute(
|
||||||
|
"Invalid Reference syntax".to_string(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
SyntaxType::JSON_FILTER => {
|
SyntaxType::JSON_FILTER => {
|
||||||
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())
|
||||||
|
@ -620,6 +640,7 @@ pub trait QueryServerTransaction<'a> {
|
||||||
"Invalid Url (whatwg/url) syntax".to_string(),
|
"Invalid Url (whatwg/url) syntax".to_string(),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
SyntaxType::OauthScope => Ok(PartialValue::new_oauthscope(value)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
@ -644,6 +665,19 @@ pub trait QueryServerTransaction<'a> {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
v
|
v
|
||||||
|
} else if let Some(r_map) = value.as_oauthscopemap() {
|
||||||
|
let v: Result<Vec<_>, _> = r_map
|
||||||
|
.iter()
|
||||||
|
.map(|(u, m)| {
|
||||||
|
let nv = self.uuid_to_spn(u)?;
|
||||||
|
let u = match nv {
|
||||||
|
Some(v) => v.to_proto_string_clone(),
|
||||||
|
None => ValueSet::uuid_to_proto_string(u),
|
||||||
|
};
|
||||||
|
Ok(format!("{}: {:?}", u, m))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
v
|
||||||
} else {
|
} else {
|
||||||
let v: Vec<_> = value.to_proto_string_clone_iter().collect();
|
let v: Vec<_> = value.to_proto_string_clone_iter().collect();
|
||||||
Ok(v)
|
Ok(v)
|
||||||
|
@ -2049,7 +2083,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
//
|
//
|
||||||
// NOTE: gen modlist IS schema aware and will handle multivalue
|
// NOTE: gen modlist IS schema aware and will handle multivalue
|
||||||
// correctly!
|
// correctly!
|
||||||
trace!("internal_migrate_or_create operating on {:?}", e.get_uuid());
|
admin_info!("internal_migrate_or_create operating on {:?}", e.get_uuid());
|
||||||
|
|
||||||
let filt = match e.filter_from_attrs(&[AttrString::from("uuid")]) {
|
let filt = match e.filter_from_attrs(&[AttrString::from("uuid")]) {
|
||||||
Some(f) => f,
|
Some(f) => f,
|
||||||
|
@ -2189,9 +2223,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
JSON_SCHEMA_ATTR_ACCOUNT_VALID_FROM,
|
JSON_SCHEMA_ATTR_ACCOUNT_VALID_FROM,
|
||||||
JSON_SCHEMA_ATTR_OAUTH2_RS_NAME,
|
JSON_SCHEMA_ATTR_OAUTH2_RS_NAME,
|
||||||
JSON_SCHEMA_ATTR_OAUTH2_RS_ORIGIN,
|
JSON_SCHEMA_ATTR_OAUTH2_RS_ORIGIN,
|
||||||
JSON_SCHEMA_ATTR_OAUTH2_RS_ACCOUNT_FILTER,
|
JSON_SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP,
|
||||||
|
JSON_SCHEMA_ATTR_OAUTH2_RS_IMPLICIT_SCOPES,
|
||||||
JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET,
|
JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET,
|
||||||
JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY,
|
JSON_SCHEMA_ATTR_OAUTH2_RS_TOKEN_KEY,
|
||||||
JSON_SCHEMA_CLASS_PERSON,
|
JSON_SCHEMA_CLASS_PERSON,
|
||||||
JSON_SCHEMA_CLASS_GROUP,
|
JSON_SCHEMA_CLASS_GROUP,
|
||||||
JSON_SCHEMA_CLASS_ACCOUNT,
|
JSON_SCHEMA_CLASS_ACCOUNT,
|
||||||
|
|
|
@ -4,13 +4,15 @@
|
||||||
//! these into a form for the backend that can be persistent into the [`Backend`](crate::be::Backend).
|
//! these into a form for the backend that can be persistent into the [`Backend`](crate::be::Backend).
|
||||||
|
|
||||||
use crate::be::dbvalue::{
|
use crate::be::dbvalue::{
|
||||||
DbCidV1, DbValueCredV1, DbValueEmailAddressV1, DbValueTaggedStringV1, DbValueV1,
|
DbCidV1, DbValueCredV1, DbValueEmailAddressV1, DbValueOauthScopeMapV1, DbValueTaggedStringV1,
|
||||||
|
DbValueV1,
|
||||||
};
|
};
|
||||||
use crate::credential::Credential;
|
use crate::credential::Credential;
|
||||||
use crate::repl::cid::Cid;
|
use crate::repl::cid::Cid;
|
||||||
use kanidm_proto::v1::Filter as ProtoFilter;
|
use kanidm_proto::v1::Filter as ProtoFilter;
|
||||||
|
|
||||||
use std::borrow::Borrow;
|
use std::borrow::Borrow;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
@ -43,6 +45,11 @@ lazy_static! {
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
Regex::new("^[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}$").expect("Invalid Nsunique regex found")
|
Regex::new("^[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}$").expect("Invalid Nsunique regex found")
|
||||||
};
|
};
|
||||||
|
static ref OAUTHSCOPE_RE: Regex = {
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
|
Regex::new("^[0-9a-zA-Z_]+$").expect("Invalid oauthscope regex found")
|
||||||
|
// Must not contain whitespace.
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
|
@ -136,6 +143,8 @@ pub enum SyntaxType {
|
||||||
DateTime,
|
DateTime,
|
||||||
EmailAddress,
|
EmailAddress,
|
||||||
Url,
|
Url,
|
||||||
|
OauthScope,
|
||||||
|
OauthScopeMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for SyntaxType {
|
impl TryFrom<&str> for SyntaxType {
|
||||||
|
@ -164,6 +173,8 @@ impl TryFrom<&str> for SyntaxType {
|
||||||
"DATETIME" => Ok(SyntaxType::DateTime),
|
"DATETIME" => Ok(SyntaxType::DateTime),
|
||||||
"EMAIL_ADDRESS" => Ok(SyntaxType::EmailAddress),
|
"EMAIL_ADDRESS" => Ok(SyntaxType::EmailAddress),
|
||||||
"URL" => Ok(SyntaxType::Url),
|
"URL" => Ok(SyntaxType::Url),
|
||||||
|
"OAUTH_SCOPE" => Ok(SyntaxType::OauthScope),
|
||||||
|
"OAUTH_SCOPE_MAP" => Ok(SyntaxType::OauthScopeMap),
|
||||||
_ => Err(()),
|
_ => Err(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,6 +204,8 @@ impl TryFrom<usize> for SyntaxType {
|
||||||
16 => Ok(SyntaxType::DateTime),
|
16 => Ok(SyntaxType::DateTime),
|
||||||
17 => Ok(SyntaxType::EmailAddress),
|
17 => Ok(SyntaxType::EmailAddress),
|
||||||
18 => Ok(SyntaxType::Url),
|
18 => Ok(SyntaxType::Url),
|
||||||
|
19 => Ok(SyntaxType::OauthScope),
|
||||||
|
20 => Ok(SyntaxType::OauthScopeMap),
|
||||||
_ => Err(()),
|
_ => Err(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -220,6 +233,8 @@ impl SyntaxType {
|
||||||
SyntaxType::DateTime => 16,
|
SyntaxType::DateTime => 16,
|
||||||
SyntaxType::EmailAddress => 17,
|
SyntaxType::EmailAddress => 17,
|
||||||
SyntaxType::Url => 18,
|
SyntaxType::Url => 18,
|
||||||
|
SyntaxType::OauthScope => 19,
|
||||||
|
SyntaxType::OauthScopeMap => 20,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -246,6 +261,8 @@ impl fmt::Display for SyntaxType {
|
||||||
SyntaxType::DateTime => "DATETIME",
|
SyntaxType::DateTime => "DATETIME",
|
||||||
SyntaxType::EmailAddress => "EMAIL_ADDRESS",
|
SyntaxType::EmailAddress => "EMAIL_ADDRESS",
|
||||||
SyntaxType::Url => "URL",
|
SyntaxType::Url => "URL",
|
||||||
|
SyntaxType::OauthScope => "OAUTH_SCOPE",
|
||||||
|
SyntaxType::OauthScopeMap => "OAUTH_SCOPE_MAP",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -255,6 +272,7 @@ pub enum DataValue {
|
||||||
Cred(Credential),
|
Cred(Credential),
|
||||||
SshKey(String),
|
SshKey(String),
|
||||||
SecretValue(String),
|
SecretValue(String),
|
||||||
|
OauthScopeMap(BTreeSet<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for DataValue {
|
impl std::fmt::Debug for DataValue {
|
||||||
|
@ -263,6 +281,7 @@ impl std::fmt::Debug for DataValue {
|
||||||
DataValue::Cred(_) => write!(f, "DataValue::Cred(_)"),
|
DataValue::Cred(_) => write!(f, "DataValue::Cred(_)"),
|
||||||
DataValue::SshKey(_) => write!(f, "DataValue::SshKey(_)"),
|
DataValue::SshKey(_) => write!(f, "DataValue::SshKey(_)"),
|
||||||
DataValue::SecretValue(_) => write!(f, "DataValue::SecretValue(_)"),
|
DataValue::SecretValue(_) => write!(f, "DataValue::SecretValue(_)"),
|
||||||
|
DataValue::OauthScopeMap(_) => write!(f, "DataValue::OauthScopeMap(_)"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -297,6 +316,8 @@ pub enum PartialValue {
|
||||||
DateTime(OffsetDateTime),
|
DateTime(OffsetDateTime),
|
||||||
EmailAddress(String),
|
EmailAddress(String),
|
||||||
Url(Url),
|
Url(Url),
|
||||||
|
OauthScope(String),
|
||||||
|
OauthScopeMap(Uuid),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SyntaxType> for PartialValue {
|
impl From<SyntaxType> for PartialValue {
|
||||||
|
@ -590,6 +611,29 @@ impl PartialValue {
|
||||||
matches!(self, PartialValue::Url(_))
|
matches!(self, PartialValue::Url(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_oauthscope(s: &str) -> Self {
|
||||||
|
PartialValue::OauthScope(s.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_oauthscope(&self) -> bool {
|
||||||
|
matches!(self, PartialValue::OauthScope(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_oauthscopemap(u: Uuid) -> Self {
|
||||||
|
PartialValue::OauthScopeMap(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_oauthscopemap_s(us: &str) -> Option<Self> {
|
||||||
|
match Uuid::parse_str(us) {
|
||||||
|
Ok(u) => Some(PartialValue::OauthScopeMap(u)),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_oauthscopemap(&self) -> bool {
|
||||||
|
matches!(self, PartialValue::OauthScopeMap(_))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_str(&self) -> Option<&str> {
|
pub fn to_str(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
PartialValue::Utf8(s) => Some(s.as_str()),
|
PartialValue::Utf8(s) => Some(s.as_str()),
|
||||||
|
@ -652,6 +696,8 @@ impl PartialValue {
|
||||||
odt.format(time::Format::Rfc3339)
|
odt.format(time::Format::Rfc3339)
|
||||||
}
|
}
|
||||||
PartialValue::Url(u) => u.to_string(),
|
PartialValue::Url(u) => u.to_string(),
|
||||||
|
PartialValue::OauthScope(u) => u.to_string(),
|
||||||
|
PartialValue::OauthScopeMap(u) => u.to_hyphenated_ref().to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1191,6 +1237,28 @@ impl Value {
|
||||||
self.pv.is_url()
|
self.pv.is_url()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_oauthscope(s: &str) -> Self {
|
||||||
|
Value {
|
||||||
|
pv: PartialValue::new_oauthscope(s),
|
||||||
|
data: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_oauthscope(&self) -> bool {
|
||||||
|
self.pv.is_oauthscope()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_oauthscopemap(u: Uuid, m: BTreeSet<String>) -> Self {
|
||||||
|
Value {
|
||||||
|
pv: PartialValue::new_oauthscopemap(u),
|
||||||
|
data: Some(Box::new(DataValue::OauthScopeMap(m))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_oauthscopemap(&self) -> bool {
|
||||||
|
self.pv.is_oauthscopemap()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn lessthan(&self, s: &PartialValue) -> bool {
|
pub fn lessthan(&self, s: &PartialValue) -> bool {
|
||||||
self.pv.lessthan(s)
|
self.pv.lessthan(s)
|
||||||
}
|
}
|
||||||
|
@ -1294,6 +1362,16 @@ impl Value {
|
||||||
pv: PartialValue::Url(u),
|
pv: PartialValue::Url(u),
|
||||||
data: None,
|
data: None,
|
||||||
}),
|
}),
|
||||||
|
DbValueV1::OauthScope(s) => Ok(Value {
|
||||||
|
pv: PartialValue::OauthScope(s),
|
||||||
|
data: None,
|
||||||
|
}),
|
||||||
|
DbValueV1::OauthScopeMap(osm) => Ok(Value {
|
||||||
|
pv: PartialValue::OauthScopeMap(osm.refer),
|
||||||
|
data: Some(Box::new(DataValue::OauthScopeMap(
|
||||||
|
osm.data.into_iter().collect(),
|
||||||
|
))),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1369,6 +1447,17 @@ impl Value {
|
||||||
DbValueV1::EmailAddress(DbValueEmailAddressV1 { d: mail.clone() })
|
DbValueV1::EmailAddress(DbValueEmailAddressV1 { d: mail.clone() })
|
||||||
}
|
}
|
||||||
PartialValue::Url(u) => DbValueV1::Url(u.clone()),
|
PartialValue::Url(u) => DbValueV1::Url(u.clone()),
|
||||||
|
PartialValue::OauthScope(s) => DbValueV1::OauthScope(s.clone()),
|
||||||
|
PartialValue::OauthScopeMap(u) => {
|
||||||
|
let data = match &self.data {
|
||||||
|
Some(v) => match v.as_ref() {
|
||||||
|
DataValue::OauthScopeMap(m) => m.iter().cloned().collect(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
None => unreachable!(),
|
||||||
|
};
|
||||||
|
DbValueV1::OauthScopeMap(DbValueOauthScopeMapV1 { refer: *u, data })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1402,6 +1491,7 @@ impl Value {
|
||||||
pub fn to_ref_uuid(&self) -> Option<&Uuid> {
|
pub fn to_ref_uuid(&self) -> Option<&Uuid> {
|
||||||
match &self.pv {
|
match &self.pv {
|
||||||
PartialValue::Refer(u) => Some(u),
|
PartialValue::Refer(u) => Some(u),
|
||||||
|
PartialValue::OauthScopeMap(u) => Some(u),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1517,6 +1607,20 @@ impl Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_oauthscope(self) -> Option<String> {
|
||||||
|
match self.pv {
|
||||||
|
PartialValue::OauthScope(s) => Some(s),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_oauthscopemap(self) -> Option<(Uuid, BTreeSet<String>)> {
|
||||||
|
match (self.pv, self.data.map(|b| (*b).clone())) {
|
||||||
|
(PartialValue::OauthScopeMap(u), Some(DataValue::OauthScopeMap(m))) => Some((u, m)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn migrate_iutf8_iname(self) -> Option<Self> {
|
pub fn migrate_iutf8_iname(self) -> Option<Self> {
|
||||||
match self.pv {
|
match self.pv {
|
||||||
PartialValue::Iutf8(v) => Some(Value {
|
PartialValue::Iutf8(v) => Some(Value {
|
||||||
|
@ -1580,6 +1684,16 @@ impl Value {
|
||||||
odt.format(time::Format::Rfc3339)
|
odt.format(time::Format::Rfc3339)
|
||||||
}
|
}
|
||||||
PartialValue::Url(u) => u.to_string(),
|
PartialValue::Url(u) => u.to_string(),
|
||||||
|
PartialValue::OauthScope(s) => s.to_string(),
|
||||||
|
PartialValue::OauthScopeMap(u) => match &self.data {
|
||||||
|
Some(v) => match v.as_ref() {
|
||||||
|
DataValue::OauthScopeMap(m) => {
|
||||||
|
format!("{}: {:?}", u, m)
|
||||||
|
}
|
||||||
|
_ => format!("{}: corrupted value tag", u),
|
||||||
|
},
|
||||||
|
None => format!("{}: corrupted value", u),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1617,6 +1731,14 @@ impl Value {
|
||||||
PartialValue::DateTime(odt) => odt.offset() == time::UtcOffset::UTC,
|
PartialValue::DateTime(odt) => odt.offset() == time::UtcOffset::UTC,
|
||||||
PartialValue::EmailAddress(mail) => validator::validate_email(mail.as_str()),
|
PartialValue::EmailAddress(mail) => validator::validate_email(mail.as_str()),
|
||||||
// PartialValue::Url validated through parsing.
|
// PartialValue::Url validated through parsing.
|
||||||
|
PartialValue::OauthScope(s) => OAUTHSCOPE_RE.is_match(s),
|
||||||
|
PartialValue::OauthScopeMap(_) => match &self.data {
|
||||||
|
Some(v) => match v.as_ref() {
|
||||||
|
DataValue::OauthScopeMap(m) => m.iter().all(|s| OAUTHSCOPE_RE.is_match(s)),
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
None => false,
|
||||||
|
},
|
||||||
_ => true,
|
_ => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1629,7 +1751,7 @@ impl Value {
|
||||||
| PartialValue::Iname(s)
|
| PartialValue::Iname(s)
|
||||||
| PartialValue::Nsuniqueid(s)
|
| PartialValue::Nsuniqueid(s)
|
||||||
| PartialValue::EmailAddress(s) => vec![s.clone()],
|
| PartialValue::EmailAddress(s) => vec![s.clone()],
|
||||||
PartialValue::Refer(u) | PartialValue::Uuid(u) => {
|
PartialValue::Refer(u) | PartialValue::Uuid(u) | PartialValue::OauthScopeMap(u) => {
|
||||||
vec![u.to_hyphenated_ref().to_string()]
|
vec![u.to_hyphenated_ref().to_string()]
|
||||||
}
|
}
|
||||||
PartialValue::Bool(b) => vec![b.to_string()],
|
PartialValue::Bool(b) => vec![b.to_string()],
|
||||||
|
@ -1651,6 +1773,7 @@ impl Value {
|
||||||
vec![odt.format(time::Format::Rfc3339)]
|
vec![odt.format(time::Format::Rfc3339)]
|
||||||
}
|
}
|
||||||
PartialValue::Url(u) => vec![u.to_string()],
|
PartialValue::Url(u) => vec![u.to_string()],
|
||||||
|
PartialValue::OauthScope(_) => vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,8 @@ use time::OffsetDateTime;
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
|
||||||
use crate::be::dbvalue::{
|
use crate::be::dbvalue::{
|
||||||
DbCidV1, DbValueCredV1, DbValueEmailAddressV1, DbValueTaggedStringV1, DbValueV1,
|
DbCidV1, DbValueCredV1, DbValueEmailAddressV1, DbValueOauthScopeMapV1, DbValueTaggedStringV1,
|
||||||
|
DbValueV1,
|
||||||
};
|
};
|
||||||
use crate::value::DataValue;
|
use crate::value::DataValue;
|
||||||
|
|
||||||
|
@ -22,7 +23,6 @@ use crate::value::DataValue;
|
||||||
enum I {
|
enum I {
|
||||||
Utf8(BTreeSet<String>),
|
Utf8(BTreeSet<String>),
|
||||||
Iutf8(BTreeSet<String>),
|
Iutf8(BTreeSet<String>),
|
||||||
// Could be AttrString?
|
|
||||||
Iname(BTreeSet<String>),
|
Iname(BTreeSet<String>),
|
||||||
Uuid(BTreeSet<Uuid>),
|
Uuid(BTreeSet<Uuid>),
|
||||||
Bool(SmolSet<[bool; 1]>),
|
Bool(SmolSet<[bool; 1]>),
|
||||||
|
@ -40,6 +40,8 @@ enum I {
|
||||||
DateTime(SmolSet<[OffsetDateTime; 1]>),
|
DateTime(SmolSet<[OffsetDateTime; 1]>),
|
||||||
EmailAddress(BTreeSet<String>),
|
EmailAddress(BTreeSet<String>),
|
||||||
Url(SmolSet<[Url; 1]>),
|
Url(SmolSet<[Url; 1]>),
|
||||||
|
OauthScope(BTreeSet<String>),
|
||||||
|
OauthScopeMap(BTreeMap<Uuid, BTreeSet<String>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ValueSet {
|
pub struct ValueSet {
|
||||||
|
@ -110,6 +112,11 @@ impl ValueSet {
|
||||||
PartialValue::DateTime(dt) => I::DateTime(smolset![dt]),
|
PartialValue::DateTime(dt) => I::DateTime(smolset![dt]),
|
||||||
PartialValue::EmailAddress(e) => I::EmailAddress(btreeset![e]),
|
PartialValue::EmailAddress(e) => I::EmailAddress(btreeset![e]),
|
||||||
PartialValue::Url(u) => I::Url(smolset![u]),
|
PartialValue::Url(u) => I::Url(smolset![u]),
|
||||||
|
PartialValue::OauthScope(x) => I::OauthScope(btreeset![x]),
|
||||||
|
PartialValue::OauthScopeMap(u) => match data.map(|b| (*b).clone()) {
|
||||||
|
Some(DataValue::OauthScopeMap(c)) => I::OauthScopeMap(btreemap![(u, c)]),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,6 +174,20 @@ impl ValueSet {
|
||||||
(I::DateTime(set), PartialValue::DateTime(dt)) => Ok(set.insert(dt)),
|
(I::DateTime(set), PartialValue::DateTime(dt)) => Ok(set.insert(dt)),
|
||||||
(I::EmailAddress(set), PartialValue::EmailAddress(e)) => Ok(set.insert(e)),
|
(I::EmailAddress(set), PartialValue::EmailAddress(e)) => Ok(set.insert(e)),
|
||||||
(I::Url(set), PartialValue::Url(u)) => Ok(set.insert(u)),
|
(I::Url(set), PartialValue::Url(u)) => Ok(set.insert(u)),
|
||||||
|
(I::OauthScope(set), PartialValue::OauthScope(u)) => Ok(set.insert(u)),
|
||||||
|
(I::OauthScopeMap(map), PartialValue::OauthScopeMap(u)) => {
|
||||||
|
if let BTreeEntry::Vacant(e) = map.entry(u) {
|
||||||
|
match data.map(|b| (*b).clone()) {
|
||||||
|
Some(DataValue::OauthScopeMap(k)) => Ok({
|
||||||
|
e.insert(k);
|
||||||
|
true
|
||||||
|
}),
|
||||||
|
_ => Err(OperationError::InvalidValueState),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
(_, _) => Err(OperationError::InvalidValueState),
|
(_, _) => Err(OperationError::InvalidValueState),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -242,6 +263,12 @@ impl ValueSet {
|
||||||
(I::Url(a), I::Url(b)) => {
|
(I::Url(a), I::Url(b)) => {
|
||||||
mergesets!(a, b)
|
mergesets!(a, b)
|
||||||
}
|
}
|
||||||
|
(I::OauthScope(a), I::OauthScope(b)) => {
|
||||||
|
mergesets!(a, b)
|
||||||
|
}
|
||||||
|
(I::OauthScopeMap(a), I::OauthScopeMap(b)) => {
|
||||||
|
mergemaps!(a, b)
|
||||||
|
}
|
||||||
// I think that in this case, we need to specify self / everything as we are changing
|
// I think that in this case, we need to specify self / everything as we are changing
|
||||||
// type and we need to potentially purge everything, so we just return the left side.
|
// type and we need to potentially purge everything, so we just return the left side.
|
||||||
_ => Err(OperationError::InvalidValueState),
|
_ => Err(OperationError::InvalidValueState),
|
||||||
|
@ -328,6 +355,12 @@ impl ValueSet {
|
||||||
set.insert(i);
|
set.insert(i);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
I::OauthScope(set) => {
|
||||||
|
set.extend(iter.filter_map(|v| v.to_oauthscope()));
|
||||||
|
}
|
||||||
|
I::OauthScopeMap(map) => {
|
||||||
|
map.extend(iter.filter_map(|v| v.to_oauthscopemap()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -390,6 +423,12 @@ impl ValueSet {
|
||||||
I::Url(set) => {
|
I::Url(set) => {
|
||||||
set.clear();
|
set.clear();
|
||||||
}
|
}
|
||||||
|
I::OauthScope(set) => {
|
||||||
|
set.clear();
|
||||||
|
}
|
||||||
|
I::OauthScopeMap(map) => {
|
||||||
|
map.clear();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
debug_assert!(self.is_empty());
|
debug_assert!(self.is_empty());
|
||||||
}
|
}
|
||||||
|
@ -454,6 +493,13 @@ impl ValueSet {
|
||||||
(I::Url(set), PartialValue::Url(u)) => {
|
(I::Url(set), PartialValue::Url(u)) => {
|
||||||
set.remove(u);
|
set.remove(u);
|
||||||
}
|
}
|
||||||
|
(I::OauthScope(set), PartialValue::OauthScope(u)) => {
|
||||||
|
set.remove(u);
|
||||||
|
}
|
||||||
|
(I::OauthScopeMap(set), PartialValue::OauthScopeMap(u))
|
||||||
|
| (I::OauthScopeMap(set), PartialValue::Refer(u)) => {
|
||||||
|
set.remove(u);
|
||||||
|
}
|
||||||
(_, _) => {
|
(_, _) => {
|
||||||
debug_assert!(false)
|
debug_assert!(false)
|
||||||
}
|
}
|
||||||
|
@ -484,6 +530,9 @@ impl ValueSet {
|
||||||
(I::DateTime(set), PartialValue::DateTime(dt)) => set.contains(dt),
|
(I::DateTime(set), PartialValue::DateTime(dt)) => set.contains(dt),
|
||||||
(I::EmailAddress(set), PartialValue::EmailAddress(e)) => set.contains(e.as_str()),
|
(I::EmailAddress(set), PartialValue::EmailAddress(e)) => set.contains(e.as_str()),
|
||||||
(I::Url(set), PartialValue::Url(u)) => set.contains(u),
|
(I::Url(set), PartialValue::Url(u)) => set.contains(u),
|
||||||
|
(I::OauthScope(set), PartialValue::OauthScope(u)) => set.contains(u),
|
||||||
|
(I::OauthScopeMap(map), PartialValue::OauthScopeMap(u))
|
||||||
|
| (I::OauthScopeMap(map), PartialValue::Refer(u)) => map.contains_key(u),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -526,6 +575,8 @@ impl ValueSet {
|
||||||
I::DateTime(set) => set.len(),
|
I::DateTime(set) => set.len(),
|
||||||
I::EmailAddress(set) => set.len(),
|
I::EmailAddress(set) => set.len(),
|
||||||
I::Url(set) => set.len(),
|
I::Url(set) => set.len(),
|
||||||
|
I::OauthScope(set) => set.len(),
|
||||||
|
I::OauthScopeMap(set) => set.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -574,6 +625,13 @@ impl ValueSet {
|
||||||
I::EmailAddress(set) => set.iter().cloned().collect(),
|
I::EmailAddress(set) => set.iter().cloned().collect(),
|
||||||
// Don't you dare comment on this quinn, it's a URL not a str.
|
// Don't you dare comment on this quinn, it's a URL not a str.
|
||||||
I::Url(set) => set.iter().map(|u| u.to_string()).collect(),
|
I::Url(set) => set.iter().map(|u| u.to_string()).collect(),
|
||||||
|
// Should we index this?
|
||||||
|
// I::OauthScope(set) => set.iter().map(|u| u.to_string()).collect(),
|
||||||
|
I::OauthScope(_set) => vec![],
|
||||||
|
I::OauthScopeMap(map) => map
|
||||||
|
.keys()
|
||||||
|
.map(|u| u.to_hyphenated_ref().to_string())
|
||||||
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -756,6 +814,30 @@ impl ValueSet {
|
||||||
Some(ValueSet { inner: I::Url(x) })
|
Some(ValueSet { inner: I::Url(x) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
(I::OauthScope(a), I::OauthScope(b)) => {
|
||||||
|
let x: BTreeSet<_> = a.difference(b).cloned().collect();
|
||||||
|
if x.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(ValueSet {
|
||||||
|
inner: I::OauthScope(x),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(I::OauthScopeMap(a), I::OauthScopeMap(b)) => {
|
||||||
|
let x: BTreeMap<_, _> = a
|
||||||
|
.iter()
|
||||||
|
.filter(|(k, _)| b.contains_key(k))
|
||||||
|
.map(|(k, v)| (k.clone(), v.clone()))
|
||||||
|
.collect();
|
||||||
|
if x.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(ValueSet {
|
||||||
|
inner: I::OauthScopeMap(x),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
// I think that in this case, we need to specify self / everything as we are changing
|
// I think that in this case, we need to specify self / everything as we are changing
|
||||||
// type and we need to potentially purge everything, so we just return the left side.
|
// type and we need to potentially purge everything, so we just return the left side.
|
||||||
_ => Some(self.clone()),
|
_ => Some(self.clone()),
|
||||||
|
@ -858,6 +940,17 @@ impl ValueSet {
|
||||||
.map(|s| s.as_str())
|
.map(|s| s.as_str())
|
||||||
.map(Value::new_email_address_s),
|
.map(Value::new_email_address_s),
|
||||||
I::Url(set) => set.iter().take(1).next().cloned().map(Value::new_url),
|
I::Url(set) => set.iter().take(1).next().cloned().map(Value::new_url),
|
||||||
|
I::OauthScope(set) => set
|
||||||
|
.iter()
|
||||||
|
.take(1)
|
||||||
|
.next()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.map(Value::new_oauthscope),
|
||||||
|
I::OauthScopeMap(map) => map
|
||||||
|
.iter()
|
||||||
|
.take(1)
|
||||||
|
.next()
|
||||||
|
.map(|(u, s)| Value::new_oauthscopemap(*u, s.clone())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -882,6 +975,19 @@ impl ValueSet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_refer_single(&self) -> Option<&Uuid> {
|
||||||
|
match &self.inner {
|
||||||
|
I::Refer(set) => {
|
||||||
|
if set.len() == 1 {
|
||||||
|
set.iter().take(1).next()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_bool_single(&self) -> Option<bool> {
|
pub fn to_bool_single(&self) -> Option<bool> {
|
||||||
match &self.inner {
|
match &self.inner {
|
||||||
I::Bool(set) => {
|
I::Bool(set) => {
|
||||||
|
@ -1022,9 +1128,10 @@ impl ValueSet {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Value::Refer
|
// Value::Refer
|
||||||
pub fn as_ref_uuid_iter(&self) -> Option<impl Iterator<Item = &Uuid>> {
|
pub fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = &Uuid> + '_>> {
|
||||||
match &self.inner {
|
match &self.inner {
|
||||||
I::Refer(set) => Some(set.iter()),
|
I::Refer(set) => Some(Box::new(set.iter())),
|
||||||
|
I::OauthScopeMap(map) => Some(Box::new(map.keys())),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1036,6 +1143,20 @@ impl ValueSet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_oauthscope_iter(&self) -> Option<impl Iterator<Item = &str>> {
|
||||||
|
match &self.inner {
|
||||||
|
I::OauthScope(set) => Some(set.iter().map(|s| s.as_str())),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_oauthscopemap(&self) -> Option<&BTreeMap<Uuid, BTreeSet<String>>> {
|
||||||
|
match &self.inner {
|
||||||
|
I::OauthScopeMap(map) => Some(map),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_proto_string_clone_iter(&self) -> ProtoIter<'_> {
|
pub fn to_proto_string_clone_iter(&self) -> ProtoIter<'_> {
|
||||||
// to_proto_string_clone
|
// to_proto_string_clone
|
||||||
match &self.inner {
|
match &self.inner {
|
||||||
|
@ -1058,6 +1179,8 @@ impl ValueSet {
|
||||||
I::DateTime(set) => ProtoIter::DateTime(set.iter()),
|
I::DateTime(set) => ProtoIter::DateTime(set.iter()),
|
||||||
I::EmailAddress(set) => ProtoIter::EmailAddress(set.iter()),
|
I::EmailAddress(set) => ProtoIter::EmailAddress(set.iter()),
|
||||||
I::Url(set) => ProtoIter::Url(set.iter()),
|
I::Url(set) => ProtoIter::Url(set.iter()),
|
||||||
|
I::OauthScope(set) => ProtoIter::OauthScope(set.iter()),
|
||||||
|
I::OauthScopeMap(set) => ProtoIter::OauthScopeMap(set.iter()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1082,6 +1205,8 @@ impl ValueSet {
|
||||||
I::DateTime(set) => DbValueV1Iter::DateTime(set.iter()),
|
I::DateTime(set) => DbValueV1Iter::DateTime(set.iter()),
|
||||||
I::EmailAddress(set) => DbValueV1Iter::EmailAddress(set.iter()),
|
I::EmailAddress(set) => DbValueV1Iter::EmailAddress(set.iter()),
|
||||||
I::Url(set) => DbValueV1Iter::Url(set.iter()),
|
I::Url(set) => DbValueV1Iter::Url(set.iter()),
|
||||||
|
I::OauthScope(set) => DbValueV1Iter::OauthScope(set.iter()),
|
||||||
|
I::OauthScopeMap(set) => DbValueV1Iter::OauthScopeMap(set.iter()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1106,6 +1231,8 @@ impl ValueSet {
|
||||||
I::DateTime(set) => PartialValueIter::DateTime(set.iter()),
|
I::DateTime(set) => PartialValueIter::DateTime(set.iter()),
|
||||||
I::EmailAddress(set) => PartialValueIter::EmailAddress(set.iter()),
|
I::EmailAddress(set) => PartialValueIter::EmailAddress(set.iter()),
|
||||||
I::Url(set) => PartialValueIter::Url(set.iter()),
|
I::Url(set) => PartialValueIter::Url(set.iter()),
|
||||||
|
I::OauthScope(set) => PartialValueIter::OauthScope(set.iter()),
|
||||||
|
I::OauthScopeMap(set) => PartialValueIter::OauthScopeMap(set.iter()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1130,6 +1257,8 @@ impl ValueSet {
|
||||||
I::DateTime(set) => ValueIter::DateTime(set.iter()),
|
I::DateTime(set) => ValueIter::DateTime(set.iter()),
|
||||||
I::EmailAddress(set) => ValueIter::EmailAddress(set.iter()),
|
I::EmailAddress(set) => ValueIter::EmailAddress(set.iter()),
|
||||||
I::Url(set) => ValueIter::Url(set.iter()),
|
I::Url(set) => ValueIter::Url(set.iter()),
|
||||||
|
I::OauthScope(set) => ValueIter::OauthScope(set.iter()),
|
||||||
|
I::OauthScopeMap(set) => ValueIter::OauthScopeMap(set.iter()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1230,6 +1359,14 @@ impl ValueSet {
|
||||||
matches!(self.inner, I::Url(_))
|
matches!(self.inner, I::Url(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_oauthscope(&self) -> bool {
|
||||||
|
matches!(self.inner, I::OauthScope(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_oauthscopemap(&self) -> bool {
|
||||||
|
matches!(self.inner, I::OauthScopeMap(_))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn migrate_iutf8_iname(&mut self) -> Result<(), OperationError> {
|
pub fn migrate_iutf8_iname(&mut self) -> Result<(), OperationError> {
|
||||||
// Swap iutf8 to Iname internally.
|
// Swap iutf8 to Iname internally.
|
||||||
let ninner = match &self.inner {
|
let ninner = match &self.inner {
|
||||||
|
@ -1269,6 +1406,8 @@ impl PartialEq for ValueSet {
|
||||||
(I::DateTime(a), I::DateTime(b)) => a.eq(b),
|
(I::DateTime(a), I::DateTime(b)) => a.eq(b),
|
||||||
(I::EmailAddress(a), I::EmailAddress(b)) => a.eq(b),
|
(I::EmailAddress(a), I::EmailAddress(b)) => a.eq(b),
|
||||||
(I::Url(a), I::Url(b)) => a.eq(b),
|
(I::Url(a), I::Url(b)) => a.eq(b),
|
||||||
|
(I::OauthScope(a), I::OauthScope(b)) => a.eq(b),
|
||||||
|
(I::OauthScopeMap(a), I::OauthScopeMap(b)) => a.eq(b),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1329,6 +1468,8 @@ pub enum ValueIter<'a> {
|
||||||
DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>),
|
DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>),
|
||||||
EmailAddress(std::collections::btree_set::Iter<'a, String>),
|
EmailAddress(std::collections::btree_set::Iter<'a, String>),
|
||||||
Url(SmolSetIter<'a, [Url; 1]>),
|
Url(SmolSetIter<'a, [Url; 1]>),
|
||||||
|
OauthScope(std::collections::btree_set::Iter<'a, String>),
|
||||||
|
OauthScopeMap(std::collections::btree_map::Iter<'a, Uuid, BTreeSet<String>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Iterator for ValueIter<'a> {
|
impl<'a> Iterator for ValueIter<'a> {
|
||||||
|
@ -1372,6 +1513,10 @@ impl<'a> Iterator for ValueIter<'a> {
|
||||||
iter.next().map(|i| Value::new_email_address_s(i.as_str()))
|
iter.next().map(|i| Value::new_email_address_s(i.as_str()))
|
||||||
}
|
}
|
||||||
ValueIter::Url(iter) => iter.next().map(|i| Value::from(i.clone())),
|
ValueIter::Url(iter) => iter.next().map(|i| Value::from(i.clone())),
|
||||||
|
ValueIter::OauthScope(iter) => iter.next().map(|i| Value::new_oauthscope(i)),
|
||||||
|
ValueIter::OauthScopeMap(iter) => iter
|
||||||
|
.next()
|
||||||
|
.map(|(group, scopes)| Value::new_oauthscopemap(*group, scopes.clone())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1396,6 +1541,8 @@ pub enum PartialValueIter<'a> {
|
||||||
DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>),
|
DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>),
|
||||||
EmailAddress(std::collections::btree_set::Iter<'a, String>),
|
EmailAddress(std::collections::btree_set::Iter<'a, String>),
|
||||||
Url(SmolSetIter<'a, [Url; 1]>),
|
Url(SmolSetIter<'a, [Url; 1]>),
|
||||||
|
OauthScope(std::collections::btree_set::Iter<'a, String>),
|
||||||
|
OauthScopeMap(std::collections::btree_map::Iter<'a, Uuid, BTreeSet<String>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Iterator for PartialValueIter<'a> {
|
impl<'a> Iterator for PartialValueIter<'a> {
|
||||||
|
@ -1447,6 +1594,12 @@ impl<'a> Iterator for PartialValueIter<'a> {
|
||||||
.next()
|
.next()
|
||||||
.map(|i| PartialValue::new_email_address_s(i.as_str())),
|
.map(|i| PartialValue::new_email_address_s(i.as_str())),
|
||||||
PartialValueIter::Url(iter) => iter.next().map(|i| PartialValue::from(i.clone())),
|
PartialValueIter::Url(iter) => iter.next().map(|i| PartialValue::from(i.clone())),
|
||||||
|
PartialValueIter::OauthScope(iter) => {
|
||||||
|
iter.next().map(|i| PartialValue::new_oauthscope(i))
|
||||||
|
}
|
||||||
|
PartialValueIter::OauthScopeMap(iter) => iter
|
||||||
|
.next()
|
||||||
|
.map(|(group, _scopes)| PartialValue::new_oauthscopemap(*group)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1471,6 +1624,8 @@ pub enum DbValueV1Iter<'a> {
|
||||||
DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>),
|
DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>),
|
||||||
EmailAddress(std::collections::btree_set::Iter<'a, String>),
|
EmailAddress(std::collections::btree_set::Iter<'a, String>),
|
||||||
Url(SmolSetIter<'a, [Url; 1]>),
|
Url(SmolSetIter<'a, [Url; 1]>),
|
||||||
|
OauthScope(std::collections::btree_set::Iter<'a, String>),
|
||||||
|
OauthScopeMap(std::collections::btree_map::Iter<'a, Uuid, BTreeSet<String>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Iterator for DbValueV1Iter<'a> {
|
impl<'a> Iterator for DbValueV1Iter<'a> {
|
||||||
|
@ -1529,6 +1684,15 @@ impl<'a> Iterator for DbValueV1Iter<'a> {
|
||||||
.next()
|
.next()
|
||||||
.map(|i| DbValueV1::EmailAddress(DbValueEmailAddressV1 { d: i.clone() })),
|
.map(|i| DbValueV1::EmailAddress(DbValueEmailAddressV1 { d: i.clone() })),
|
||||||
DbValueV1Iter::Url(iter) => iter.next().map(|i| DbValueV1::Url(i.clone())),
|
DbValueV1Iter::Url(iter) => iter.next().map(|i| DbValueV1::Url(i.clone())),
|
||||||
|
DbValueV1Iter::OauthScope(iter) => {
|
||||||
|
iter.next().map(|i| DbValueV1::OauthScope(i.clone()))
|
||||||
|
}
|
||||||
|
DbValueV1Iter::OauthScopeMap(iter) => iter.next().map(|(u, m)| {
|
||||||
|
DbValueV1::OauthScopeMap(DbValueOauthScopeMapV1 {
|
||||||
|
refer: *u,
|
||||||
|
data: m.iter().cloned().collect(),
|
||||||
|
})
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1553,6 +1717,8 @@ pub enum ProtoIter<'a> {
|
||||||
DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>),
|
DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>),
|
||||||
EmailAddress(std::collections::btree_set::Iter<'a, String>),
|
EmailAddress(std::collections::btree_set::Iter<'a, String>),
|
||||||
Url(SmolSetIter<'a, [Url; 1]>),
|
Url(SmolSetIter<'a, [Url; 1]>),
|
||||||
|
OauthScope(std::collections::btree_set::Iter<'a, String>),
|
||||||
|
OauthScopeMap(std::collections::btree_map::Iter<'a, Uuid, BTreeSet<String>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Iterator for ProtoIter<'a> {
|
impl<'a> Iterator for ProtoIter<'a> {
|
||||||
|
@ -1600,6 +1766,10 @@ impl<'a> Iterator for ProtoIter<'a> {
|
||||||
}),
|
}),
|
||||||
ProtoIter::EmailAddress(iter) => iter.next().cloned(),
|
ProtoIter::EmailAddress(iter) => iter.next().cloned(),
|
||||||
ProtoIter::Url(iter) => iter.next().map(|i| i.to_string()),
|
ProtoIter::Url(iter) => iter.next().map(|i| i.to_string()),
|
||||||
|
ProtoIter::OauthScope(iter) => iter.next().cloned(),
|
||||||
|
ProtoIter::OauthScopeMap(iter) => iter
|
||||||
|
.next()
|
||||||
|
.map(|(u, m)| format!("{}: {:?}", ValueSet::uuid_to_proto_string(u), m)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue