mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +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:
|
||||
@docker buildx build $(EXT_OPTS) --pull --push --platform "linux/amd64" \
|
||||
--allow security.insecure \
|
||||
-f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:x86_64_$(IMAGE_VERSION) \
|
||||
--build-arg "KANIDM_BUILD_PROFILE=container_x86_64_v3" \
|
||||
--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:
|
||||
@docker buildx build $(EXT_OPTS) --pull --push --platform "linux/amd64" \
|
||||
--allow security.insecure \
|
||||
-f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:x86_64_$(IMAGE_VERSION) \
|
||||
--build-arg "KANIDM_BUILD_PROFILE=container_x86_64_v3" \
|
||||
--build-arg "KANIDM_FEATURES=" \
|
||||
|
@ -33,7 +31,6 @@ buildx/kanidmd/x86_64_v3:
|
|||
buildx/kanidmd: ## build multiarch server images
|
||||
buildx/kanidmd:
|
||||
@docker buildx build $(EXT_OPTS) --pull --push --platform $(IMAGE_ARCH) \
|
||||
--allow security.insecure \
|
||||
-f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:$(IMAGE_VERSION) \
|
||||
--build-arg "KANIDM_BUILD_PROFILE=container_generic" \
|
||||
--build-arg "KANIDM_FEATURES=" \
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
- [POSIX Accounts and Groups](./posix_accounts.md)
|
||||
- [SSH Key Distribution](./ssh_key_dist.md)
|
||||
- [The Recycle Bin](./recycle_bin.md)
|
||||
- [LDAP](./ldap.md)
|
||||
- [Oauth2](./oauth2.md)
|
||||
- [PAM and nsswitch](./pam_and_nsswitch.md)
|
||||
- [RADIUS](./radius.md)
|
||||
- [LDAP](./ldap.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 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` |
|
||||
| --- | --- |
|
||||
|
|
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 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 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 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
|
||||
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,
|
||||
|
|
|
@ -1378,12 +1378,16 @@ impl KanidmAsyncClient {
|
|||
pub async fn idm_oauth2_rs_basic_create(
|
||||
&self,
|
||||
name: &str,
|
||||
displayname: &str,
|
||||
origin: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
let mut new_oauth2_rs = Entry::default();
|
||||
new_oauth2_rs
|
||||
.attrs
|
||||
.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
|
||||
.attrs
|
||||
.insert("oauth2_rs_origin".to_string(), vec![origin.to_string()]);
|
||||
|
@ -1400,7 +1404,9 @@ impl KanidmAsyncClient {
|
|||
&self,
|
||||
id: &str,
|
||||
name: Option<&str>,
|
||||
displayname: Option<&str>,
|
||||
origin: Option<&str>,
|
||||
scopes: Option<Vec<&str>>,
|
||||
reset_secret: bool,
|
||||
reset_token_key: bool,
|
||||
) -> Result<(), ClientError> {
|
||||
|
@ -1413,11 +1419,22 @@ impl KanidmAsyncClient {
|
|||
.attrs
|
||||
.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 {
|
||||
update_oauth2_rs
|
||||
.attrs
|
||||
.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 {
|
||||
update_oauth2_rs
|
||||
.attrs
|
||||
|
@ -1426,13 +1443,36 @@ impl KanidmAsyncClient {
|
|||
if reset_token_key {
|
||||
update_oauth2_rs
|
||||
.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)
|
||||
.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> {
|
||||
self.perform_delete_request(["/v1/oauth2/", id].concat().as_str())
|
||||
.await
|
||||
|
|
|
@ -891,8 +891,16 @@ impl KanidmClient {
|
|||
tokio_block_on(self.asclient.idm_oauth2_rs_list())
|
||||
}
|
||||
|
||||
pub fn idm_oauth2_rs_basic_create(&self, name: &str, origin: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_basic_create(name, origin))
|
||||
pub fn idm_oauth2_rs_basic_create(
|
||||
&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> {
|
||||
|
@ -903,19 +911,39 @@ impl KanidmClient {
|
|||
&self,
|
||||
id: &str,
|
||||
name: Option<&str>,
|
||||
displayname: Option<&str>,
|
||||
origin: Option<&str>,
|
||||
scopes: Option<Vec<&str>>,
|
||||
reset_secret: bool,
|
||||
reset_token_key: bool,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_update(
|
||||
id,
|
||||
name,
|
||||
displayname,
|
||||
origin,
|
||||
scopes,
|
||||
reset_secret,
|
||||
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> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_delete(id))
|
||||
}
|
||||
|
|
|
@ -39,9 +39,25 @@ fn test_oauth2_basic_flow() {
|
|||
|
||||
// Create an oauth2 application integration.
|
||||
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");
|
||||
|
||||
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
|
||||
.idm_oauth2_rs_get("test_integration")
|
||||
.ok()
|
||||
|
@ -90,7 +106,7 @@ fn test_oauth2_basic_flow() {
|
|||
("code_challenge", pkce_code_challenge.as_str()),
|
||||
("code_challenge_method", "S256"),
|
||||
("redirect_uri", "https://demo.example.com/oauth2/flow"),
|
||||
("scope", "mail+name+test"),
|
||||
("scope", "email read"),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
|
|
|
@ -1073,7 +1073,11 @@ fn test_server_rest_oauth2_basic_lifecycle() {
|
|||
|
||||
// Create a new oauth2 config
|
||||
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");
|
||||
|
||||
// List, there is what we created.
|
||||
|
@ -1091,12 +1095,12 @@ fn test_server_rest_oauth2_basic_lifecycle() {
|
|||
.flatten()
|
||||
.expect("Failed to retrieve test_integration config");
|
||||
|
||||
eprintln!("{:?}", oauth2_config);
|
||||
|
||||
// What can we see?
|
||||
assert!(oauth2_config.attrs.contains_key("oauth2_rs_basic_secret"));
|
||||
// This is present, but redacted.
|
||||
assert!(oauth2_config
|
||||
.attrs
|
||||
.contains_key("oauth2_rs_basic_token_key"));
|
||||
assert!(oauth2_config.attrs.contains_key("oauth2_rs_token_key"));
|
||||
|
||||
// Mod delete the secret/key and check them again.
|
||||
// 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(
|
||||
"test_integration",
|
||||
None,
|
||||
Some("Test Integration"),
|
||||
Some("https://new_demo.example.com"),
|
||||
Some(vec!["read", "email"]),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
|
@ -1118,6 +1124,34 @@ fn test_server_rest_oauth2_basic_lifecycle() {
|
|||
|
||||
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
|
||||
rsclient
|
||||
.idm_oauth2_rs_delete("test_integration")
|
||||
|
|
|
@ -31,6 +31,7 @@ pub struct AuthorisationRequest {
|
|||
pub struct ConsentRequest {
|
||||
// A pretty-name of the client
|
||||
pub client_name: String,
|
||||
// A list of scopes requested / to be issued.
|
||||
pub scopes: Vec<String>,
|
||||
// The users displayname (?)
|
||||
// pub display_name: String,
|
||||
|
|
|
@ -6,6 +6,10 @@ impl Oauth2Opt {
|
|||
Oauth2Opt::List(copt) => copt.debug,
|
||||
Oauth2Opt::Get(nopt) => 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,
|
||||
}
|
||||
}
|
||||
|
@ -28,14 +32,66 @@ impl Oauth2Opt {
|
|||
}
|
||||
}
|
||||
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();
|
||||
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"),
|
||||
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) => {
|
||||
let client = nopt.copt.to_client();
|
||||
match client.idm_oauth2_rs_delete(nopt.name.as_str()) {
|
||||
|
|
|
@ -354,10 +354,38 @@ pub enum SelfOpt {
|
|||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct Oauth2BasicCreateOpt {
|
||||
#[structopt(name = "origin")]
|
||||
origin: String,
|
||||
#[structopt(flatten)]
|
||||
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)]
|
||||
|
@ -374,6 +402,18 @@ pub enum Oauth2Opt {
|
|||
#[structopt(name = "create")]
|
||||
/// Create a new oauth2 resource server
|
||||
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")]
|
||||
/// Delete a oauth2 resource server
|
||||
Delete(Named),
|
||||
|
|
|
@ -1121,6 +1121,129 @@ impl QueryServerWriteV1 {
|
|||
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. =====
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
|
|
|
@ -136,6 +136,14 @@ pub struct DbValueEmailAddressV1 {
|
|||
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)]
|
||||
pub enum DbValueV1 {
|
||||
#[serde(rename = "U8")]
|
||||
|
@ -176,6 +184,10 @@ pub enum DbValueV1 {
|
|||
EmailAddress(DbValueEmailAddressV1),
|
||||
#[serde(rename = "UR")]
|
||||
Url(Url),
|
||||
#[serde(rename = "OS")]
|
||||
OauthScope(String),
|
||||
#[serde(rename = "OM")]
|
||||
OauthScopeMap(DbValueOauthScopeMapV1),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -1014,33 +1014,41 @@ pub const JSON_IDM_HP_ACP_OAUTH2_MANAGE_PRIV_V1: &str = r#"{
|
|||
"acp_search_attr": [
|
||||
"class",
|
||||
"description",
|
||||
"displayname",
|
||||
"oauth2_rs_name",
|
||||
"oauth2_rs_origin",
|
||||
"oauth2_rs_account_filter",
|
||||
"oauth2_rs_scope_map",
|
||||
"oauth2_rs_implicit_scopes",
|
||||
"oauth2_rs_basic_secret",
|
||||
"oauth2_rs_basic_token_key"
|
||||
"oauth2_rs_token_key"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"description",
|
||||
"displayname",
|
||||
"oauth2_rs_name",
|
||||
"oauth2_rs_origin",
|
||||
"oauth2_rs_account_filter",
|
||||
"oauth2_rs_scope_map",
|
||||
"oauth2_rs_implicit_scopes",
|
||||
"oauth2_rs_basic_secret",
|
||||
"oauth2_rs_basic_token_key"
|
||||
"oauth2_rs_token_key"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"description",
|
||||
"displayname",
|
||||
"oauth2_rs_name",
|
||||
"oauth2_rs_origin",
|
||||
"oauth2_rs_account_filter"
|
||||
"oauth2_rs_scope_map",
|
||||
"oauth2_rs_implicit_scopes"
|
||||
],
|
||||
"acp_modify_class": [],
|
||||
"acp_create_attr": [
|
||||
"class",
|
||||
"description",
|
||||
"displayname",
|
||||
"oauth2_rs_name",
|
||||
"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"]
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
"class": [
|
||||
"object",
|
||||
|
@ -580,20 +580,22 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_ACCOUNT_FILTER: &str = r#"{
|
|||
"attributetype"
|
||||
],
|
||||
"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": [
|
||||
"false"
|
||||
],
|
||||
"multivalue": [
|
||||
"false"
|
||||
"true"
|
||||
],
|
||||
"attributename": [
|
||||
"oauth2_rs_account_filter"
|
||||
"oauth2_rs_scope_map"
|
||||
],
|
||||
"syntax": [
|
||||
"JSON_FILTER"
|
||||
"OAUTH_SCOPE_MAP"
|
||||
],
|
||||
"uuid": [
|
||||
"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": {
|
||||
"class": [
|
||||
"object",
|
||||
|
@ -638,7 +640,7 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str = r#"{
|
|||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"An oauth2 basic resource servers unique token signing key"
|
||||
"An oauth2 resource servers unique token signing key"
|
||||
],
|
||||
"index": [],
|
||||
"unique": [
|
||||
|
@ -648,7 +650,7 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str = r#"{
|
|||
"false"
|
||||
],
|
||||
"attributename": [
|
||||
"oauth2_rs_basic_token_key"
|
||||
"oauth2_rs_token_key"
|
||||
],
|
||||
"syntax": [
|
||||
"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 ===
|
||||
|
||||
pub const JSON_SCHEMA_CLASS_PERSON: &str = r#"
|
||||
|
@ -879,11 +910,14 @@ pub const JSON_SCHEMA_CLASS_OAUTH2_RS: &str = r#"
|
|||
],
|
||||
"systemmay": [
|
||||
"description",
|
||||
"oauth2_rs_account_filter"
|
||||
"oauth2_rs_scope_map",
|
||||
"oauth2_rs_implicit_scopes"
|
||||
],
|
||||
"systemmust": [
|
||||
"oauth2_rs_name",
|
||||
"oauth2_rs_origin"
|
||||
"displayname",
|
||||
"oauth2_rs_origin",
|
||||
"oauth2_rs_token_key"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000085"
|
||||
|
@ -901,15 +935,14 @@ pub const JSON_SCHEMA_CLASS_OAUTH2_RS_BASIC: &str = r#"
|
|||
"classtype"
|
||||
],
|
||||
"description": [
|
||||
"The class representing a configured Oauth2 Resource Server"
|
||||
"The class representing a configured Oauth2 Resource Server authenticated with http basic"
|
||||
],
|
||||
"classname": [
|
||||
"oauth2_resource_server_basic"
|
||||
],
|
||||
"systemmay": [],
|
||||
"systemmust": [
|
||||
"oauth2_rs_basic_secret",
|
||||
"oauth2_rs_basic_token_key"
|
||||
"oauth2_rs_basic_secret"
|
||||
],
|
||||
"uuid": [
|
||||
"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_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_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_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";
|
||||
|
@ -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_ORIGIN: &str = "00000000-0000-0000-0000-ffff00000081";
|
||||
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_ACCOUNT_FILTER: &str =
|
||||
"00000000-0000-0000-0000-ffff00000082";
|
||||
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP: &str = "00000000-0000-0000-0000-ffff00000082";
|
||||
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET: &str =
|
||||
"00000000-0000-0000-0000-ffff00000083";
|
||||
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str =
|
||||
"00000000-0000-0000-0000-ffff00000084";
|
||||
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_TOKEN_KEY: &str = "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_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_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
|
||||
// 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! {
|
||||
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_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();
|
||||
|
|
|
@ -449,6 +449,11 @@ pub fn create_https_server(
|
|||
.patch(oauth2_id_patch)
|
||||
.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");
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
// Delete this
|
||||
let uat = req.get_current_uat();
|
||||
|
|
|
@ -1592,6 +1592,19 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
|||
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)]
|
||||
/// 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>> {
|
||||
|
@ -1600,7 +1613,7 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
|||
|
||||
#[inline(always)]
|
||||
/// 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.
|
||||
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())
|
||||
}
|
||||
|
||||
pub fn get_ava_single_refer(&self, attr: &str) -> Option<&Uuid> {
|
||||
self.attrs.get(attr).and_then(|vs| vs.to_refer_single())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
/// Return a single protocol filter, if valid to transform this value.
|
||||
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.
|
||||
match schema.is_multivalue(k) {
|
||||
Ok(r) => {
|
||||
if !r {
|
||||
if !r || k == "systemmust" || k == "systemmay" {
|
||||
// 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()));
|
||||
}
|
||||
}
|
||||
|
@ -1907,21 +1926,30 @@ where
|
|||
|
||||
/// Remove an attribute-value pair from this entry.
|
||||
fn remove_ava(&mut self, attr: &str, value: &PartialValue) {
|
||||
// It would be great to remove these extra allocations, but they
|
||||
// really don't cost much :(
|
||||
self.attrs.entry(AttrString::from(attr)).and_modify(|v| {
|
||||
// Here we need to actually do a check/binary search ...
|
||||
v.remove(value);
|
||||
});
|
||||
let rm = if let Some(vs) = self.attrs.get_mut(attr) {
|
||||
vs.remove(value);
|
||||
vs.is_empty()
|
||||
} else {
|
||||
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>) {
|
||||
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| {
|
||||
vs.remove(k);
|
||||
})
|
||||
}
|
||||
});
|
||||
vs.is_empty()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if rm {
|
||||
self.attrs.remove(attr);
|
||||
};
|
||||
}
|
||||
|
||||
/// Remove all values of this attribute from the entry.
|
||||
|
@ -2235,13 +2263,13 @@ mod tests {
|
|||
e.apply_modlist(&present_single_mods);
|
||||
assert!(e.attribute_equality("attr", &PartialValue::new_iutf8("value")));
|
||||
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;
|
||||
|
||||
e.apply_modlist(&remove_empty_mods);
|
||||
|
||||
assert!(e.attrs.get("attr").unwrap().is_empty());
|
||||
assert!(e.attrs.get("attr").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -151,4 +151,13 @@ impl Identity {
|
|||
.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 kanidm_proto::v1::UserAuthToken;
|
||||
use openssl::sha;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::trace;
|
||||
use url::{Origin, Url};
|
||||
use webauthn_rs::base64_data::Base64UrlSafeData;
|
||||
|
||||
|
@ -76,6 +78,8 @@ struct ConsentToken {
|
|||
pub code_challenge: Base64UrlSafeData,
|
||||
// Where the RS wants us to go back to.
|
||||
pub redirect_uri: Url,
|
||||
// The scopes being granted
|
||||
pub scopes: Vec<String>,
|
||||
}
|
||||
|
||||
// consent token?
|
||||
|
@ -89,6 +93,8 @@ struct TokenExchangeCode {
|
|||
pub code_challenge: Base64UrlSafeData,
|
||||
// The original redirect uri
|
||||
pub redirect_uri: Url,
|
||||
// The scopes being granted
|
||||
pub scopes: Vec<String>,
|
||||
}
|
||||
|
||||
// consentPermitResponse
|
||||
|
@ -103,32 +109,43 @@ pub struct AuthorisePermitSuccess {
|
|||
pub code: String,
|
||||
}
|
||||
|
||||
// The cache structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Oauth2RSTokenFormat {
|
||||
Uat,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Oauth2RSBasic {
|
||||
pub struct Oauth2RS {
|
||||
name: String,
|
||||
displayname: String,
|
||||
uuid: Uuid,
|
||||
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,
|
||||
// Our internal exchange encryption material for this rs.
|
||||
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 {
|
||||
f.debug_struct("Oauth2RSBasic")
|
||||
f.debug_struct("Oauth2RS")
|
||||
.field("name", &self.name)
|
||||
.field("displayname", &self.displayname)
|
||||
.field("uuid", &self.uuid)
|
||||
.field("origin", &self.origin)
|
||||
.field("scope_maps", &self.scope_maps)
|
||||
.field("implicit_scopes", &self.implicit_scopes)
|
||||
.field("token_format", &self.token_format)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Oauth2RS {
|
||||
Basic(Oauth2RSBasic),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Oauth2RSInner {
|
||||
fernet: Fernet,
|
||||
|
@ -186,44 +203,68 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
|||
let rs_set: Result<HashMap<_, _>, _> = value
|
||||
.into_iter()
|
||||
.map(|ent| {
|
||||
let uuid = *ent.get_uuid();
|
||||
admin_info!(?uuid, "Checking oauth2 configuration");
|
||||
// From each entry, attempt to make an oauth2 configuration.
|
||||
if !ent.attribute_equality("class", &CLASS_OAUTH2) {
|
||||
admin_error!("Missing class oauth2_resource_server");
|
||||
// Check we have oauth2_resource_server class
|
||||
Err(OperationError::InvalidEntryState)
|
||||
} else if ent.attribute_equality("class", &CLASS_OAUTH2_BASIC) {
|
||||
// If we have oauth2_resource_server_basic
|
||||
// Now we know we can load the attrs.
|
||||
let uuid = *ent.get_uuid();
|
||||
trace!("name");
|
||||
let name = ent
|
||||
.get_ava_single_str("oauth2_rs_name")
|
||||
.map(str::to_string)
|
||||
.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
|
||||
.get_ava_single_url("oauth2_rs_origin")
|
||||
.map(|url| url.origin())
|
||||
.ok_or(OperationError::InvalidValueState)?;
|
||||
trace!("authz_secret");
|
||||
let authz_secret = ent
|
||||
.get_ava_single_str("oauth2_rs_basic_secret")
|
||||
.map(str::to_string)
|
||||
.ok_or(OperationError::InvalidValueState)?;
|
||||
trace!("token_key");
|
||||
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)
|
||||
.and_then(|key| {
|
||||
Fernet::new(key).ok_or(OperationError::CryptographyError)
|
||||
})?;
|
||||
|
||||
// Currently unsure if this is how I want to handle this.
|
||||
// let oauth2_rs_account_filter = ent.get_ava_single_protofilter("oauth2_rs_account_filter")
|
||||
trace!("scope_maps");
|
||||
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 rscfg = Oauth2RS::Basic(Oauth2RSBasic {
|
||||
let rscfg = Oauth2RS {
|
||||
name,
|
||||
displayname,
|
||||
uuid,
|
||||
origin,
|
||||
scope_maps,
|
||||
implicit_scopes,
|
||||
authz_secret,
|
||||
token_fernet,
|
||||
});
|
||||
token_format: Oauth2RSTokenFormat::Uat,
|
||||
};
|
||||
|
||||
Ok((client_id, rscfg))
|
||||
} else {
|
||||
|
@ -275,28 +316,59 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
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
|
||||
// the user to indicate their consent to this authorisation.
|
||||
//
|
||||
// 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 {
|
||||
client_id: auth_req.client_id.clone(),
|
||||
ident_id: ident.get_event_origin_id(),
|
||||
|
@ -304,6 +376,7 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
state: auth_req.state.clone(),
|
||||
code_challenge: auth_req.code_challenge.clone(),
|
||||
redirect_uri: auth_req.redirect_uri.clone(),
|
||||
scopes: avail_scopes.clone(),
|
||||
};
|
||||
|
||||
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());
|
||||
|
||||
Ok(ConsentRequest {
|
||||
client_name: auth_req.client_id.clone(),
|
||||
scopes: Vec::new(),
|
||||
client_name: o2rs.displayname.clone(),
|
||||
scopes: avail_scopes,
|
||||
consent_token,
|
||||
})
|
||||
}
|
||||
|
@ -359,13 +432,14 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
}
|
||||
|
||||
// Get the resource server config based on this client_id.
|
||||
let o2rs_fernet = match self.inner.rs_set.get(&consent_req.client_id) {
|
||||
Some(Oauth2RS::Basic(rsbasic)) => &rsbasic.token_fernet,
|
||||
None => {
|
||||
let o2rs = self
|
||||
.inner
|
||||
.rs_set
|
||||
.get(&consent_req.client_id)
|
||||
.ok_or_else(|| {
|
||||
admin_error!("Invalid consent request oauth2 client_id");
|
||||
return Err(OperationError::InvalidRequestState);
|
||||
}
|
||||
};
|
||||
OperationError::InvalidRequestState
|
||||
})?;
|
||||
|
||||
// Extract the state, code challenge, redirect_uri
|
||||
|
||||
|
@ -373,6 +447,7 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
uat: uat.clone(),
|
||||
code_challenge: consent_req.code_challenge,
|
||||
redirect_uri: consent_req.redirect_uri.clone(),
|
||||
scopes: consent_req.scopes,
|
||||
};
|
||||
|
||||
// Encrypt the exchange token with the fernet key of the client resource server
|
||||
|
@ -381,7 +456,7 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
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 {
|
||||
redirect_uri: consent_req.redirect_uri,
|
||||
|
@ -434,21 +509,17 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
})?;
|
||||
|
||||
// check the secret.
|
||||
let o2rs_fernet = match o2rs {
|
||||
Oauth2RS::Basic(rsbasic) => {
|
||||
if rsbasic.authz_secret != secret {
|
||||
security_info!("Invalid oauth2 client_id secret");
|
||||
return Err(Oauth2Error::AuthenticationRequired);
|
||||
}
|
||||
// We are authenticated! Yay! Now we can actually check things ...
|
||||
&rsbasic.token_fernet
|
||||
}
|
||||
};
|
||||
if o2rs.authz_secret != secret {
|
||||
security_info!("Invalid oauth2 client_id secret");
|
||||
return Err(Oauth2Error::AuthenticationRequired);
|
||||
}
|
||||
// We are authenticated! Yay! Now we can actually check things ...
|
||||
|
||||
// Check the token_req is within the valid time, and correctly signed for
|
||||
// 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())
|
||||
.map_err(|_| {
|
||||
admin_error!("Failed to decrypt token exchange request");
|
||||
|
@ -491,19 +562,30 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
return Err(Oauth2Error::AccessDenied);
|
||||
};
|
||||
|
||||
let access_token = serde_json::to_vec(&code_xchg.uat)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Unable to encode uat data");
|
||||
Oauth2Error::ServerError(OperationError::SerdeJsonError)
|
||||
})
|
||||
.map(|data| o2rs_fernet.encrypt_at_time(&data, ct.as_secs()))?;
|
||||
let scope = if code_xchg.scopes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(code_xchg.scopes.join(" "))
|
||||
};
|
||||
|
||||
// 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 {
|
||||
access_token,
|
||||
token_type: "bearer".to_string(),
|
||||
expires_in,
|
||||
refresh_token: None,
|
||||
scope: None,
|
||||
scope,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -552,7 +634,7 @@ mod tests {
|
|||
code_challenge: Base64UrlSafeData($code_challenge),
|
||||
code_challenge_method: CodeChallengeMethod::S256,
|
||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||
scope: "".to_string(),
|
||||
scope: "test".to_string(),
|
||||
};
|
||||
|
||||
$idms_prox_read
|
||||
|
@ -576,9 +658,16 @@ mod tests {
|
|||
("class", Value::new_class("oauth2_resource_server_basic")),
|
||||
("uuid", Value::new_uuid(uuid)),
|
||||
("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")),
|
||||
// System admins
|
||||
(
|
||||
"oauth2_rs_scope_map",
|
||||
Value::new_oauthscopemap(*UUID_SYSTEM_ADMINS, btreeset!["read".to_string()])
|
||||
)
|
||||
);
|
||||
let ce = CreateEvent::new_internal(vec![e]);
|
||||
|
@ -610,6 +699,24 @@ mod tests {
|
|||
(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]
|
||||
fn test_idm_oauth2_basic_function() {
|
||||
run_idm_test!(|_qs: &QueryServer,
|
||||
|
@ -666,6 +773,10 @@ mod tests {
|
|||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
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 (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||
|
@ -678,7 +789,7 @@ mod tests {
|
|||
code_challenge: Base64UrlSafeData(code_challenge.clone()),
|
||||
code_challenge_method: CodeChallengeMethod::S256,
|
||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||
scope: "".to_string(),
|
||||
scope: "test".to_string(),
|
||||
};
|
||||
|
||||
assert!(
|
||||
|
@ -696,7 +807,7 @@ mod tests {
|
|||
code_challenge: Base64UrlSafeData(code_challenge.clone()),
|
||||
code_challenge_method: CodeChallengeMethod::S256,
|
||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||
scope: "".to_string(),
|
||||
scope: "test".to_string(),
|
||||
};
|
||||
|
||||
assert!(
|
||||
|
@ -711,10 +822,10 @@ mod tests {
|
|||
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: Base64UrlSafeData(code_challenge.clone()),
|
||||
code_challenge_method: CodeChallengeMethod::S256,
|
||||
redirect_uri: Url::parse("https://totes.not.sus.org/oauth2/result").unwrap(),
|
||||
scope: "".to_string(),
|
||||
scope: "test".to_string(),
|
||||
};
|
||||
|
||||
assert!(
|
||||
|
@ -723,6 +834,43 @@ mod tests {
|
|||
.unwrap_err()
|
||||
== 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> {
|
||||
// Get the account
|
||||
let account = self
|
||||
|
@ -1172,24 +1171,19 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
|
||||
// Deny the change if the account is anonymous!
|
||||
if account.is_anonymous() {
|
||||
admin_warn!("Unable to convert anonymous to account during write txn");
|
||||
Err(OperationError::SystemProtectedObject)
|
||||
} else {
|
||||
Ok(account)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: tracing
|
||||
pub fn set_account_password(
|
||||
&mut self,
|
||||
pce: &PasswordChangeEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
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.
|
||||
let modlist = account
|
||||
.gen_password_mod(pce.cleartext.as_str(), self.crypto_policy)
|
||||
|
@ -1257,7 +1251,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: tracing
|
||||
pub fn set_unix_account_password(
|
||||
&mut self,
|
||||
pce: &UnixPasswordChangeEvent,
|
||||
|
|
|
@ -100,6 +100,10 @@ impl ModifyList<ModifyInvalid> {
|
|||
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 {
|
||||
Self::new_list(vec![m_purge(attr)])
|
||||
}
|
||||
|
|
|
@ -20,11 +20,11 @@ macro_rules! oauth2_transform {
|
|||
let v = Value::new_utf8(password_from_random());
|
||||
$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");
|
||||
let k = fernet::Fernet::generate_key();
|
||||
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(())
|
||||
|
@ -68,11 +68,13 @@ mod tests {
|
|||
("class", Value::new_class("oauth2_resource_server")),
|
||||
("class", Value::new_class("oauth2_resource_server_basic")),
|
||||
("uuid", Value::new_uuid(uuid)),
|
||||
("displayname", Value::new_utf8s("test_resource_server")),
|
||||
("oauth2_rs_name", Value::new_iname("test_resource_server")),
|
||||
(
|
||||
"oauth2_rs_origin",
|
||||
Value::new_url_s("https://demo.example.com").unwrap()
|
||||
)
|
||||
),
|
||||
("oauth2_rs_implicit_scopes", Value::new_oauthscope("read"))
|
||||
);
|
||||
|
||||
let create = vec![e];
|
||||
|
@ -87,7 +89,7 @@ mod tests {
|
|||
.internal_search_uuid(&uuid)
|
||||
.expect("failed to get oauth2 config");
|
||||
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")),
|
||||
("uuid", Value::new_uuid(uuid)),
|
||||
("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("read")),
|
||||
("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];
|
||||
|
@ -118,7 +122,7 @@ mod tests {
|
|||
filter!(f_eq("uuid", PartialValue::new_uuid(uuid))),
|
||||
ModifyList::new_list(vec![
|
||||
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,
|
||||
|qs: &QueryServerWriteTransaction| {
|
||||
|
@ -126,10 +130,10 @@ mod tests {
|
|||
.internal_search_uuid(&uuid)
|
||||
.expect("failed to get oauth2 config");
|
||||
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.
|
||||
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 kanidm_proto::v1::{ConsistencyError, PluginError};
|
||||
use std::sync::Arc;
|
||||
use tracing::trace;
|
||||
|
||||
// NOTE: This *must* be after base.rs!!!
|
||||
|
||||
|
@ -33,7 +34,7 @@ impl ReferentialIntegrity {
|
|||
) -> Result<(), OperationError> {
|
||||
if inner.is_empty() {
|
||||
// There is nothing to check! Move on.
|
||||
admin_info!("no reference types modified, skipping check");
|
||||
trace!("no reference types modified, skipping check");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
@ -109,7 +110,7 @@ impl Plugin for ReferentialIntegrity {
|
|||
});
|
||||
Ok(())
|
||||
} 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.");
|
||||
Err(OperationError::InvalidAttribute(
|
||||
"uuid could not become reference value".to_string(),
|
||||
|
@ -144,7 +145,7 @@ impl Plugin for ReferentialIntegrity {
|
|||
v.to_ref_uuid()
|
||||
.map(|uuid| PartialValue::new_uuid(*uuid))
|
||||
.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.");
|
||||
OperationError::InvalidAttribute(
|
||||
"uuid could not become reference value".to_string(),
|
||||
|
@ -698,4 +699,61 @@ mod tests {
|
|||
|_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::EmailAddress => v.is_email_address(),
|
||||
SyntaxType::Url => v.is_url(),
|
||||
SyntaxType::OauthScope => v.is_oauthscope(),
|
||||
SyntaxType::OauthScopeMap => v.is_oauthscopemap() || v.is_refer(),
|
||||
};
|
||||
if r {
|
||||
Ok(())
|
||||
} else {
|
||||
trace!(?a, ?self, ?v, "validate_pv InvalidAttributeSyntax");
|
||||
Err(SchemaError::InvalidAttributeSyntax(a.to_string()))
|
||||
}
|
||||
}
|
||||
|
@ -213,6 +216,12 @@ impl SchemaAttribute {
|
|||
let pv: &PartialValue = v.borrow();
|
||||
self.validate_partialvalue(a, pv)
|
||||
} else {
|
||||
trace!(
|
||||
?a,
|
||||
?self,
|
||||
?v,
|
||||
"value validation failure - InvalidAttributeSyntax"
|
||||
);
|
||||
Err(SchemaError::InvalidAttributeSyntax(a.to_string()))
|
||||
}
|
||||
}
|
||||
|
@ -222,6 +231,7 @@ impl SchemaAttribute {
|
|||
// Check multivalue
|
||||
if !self.multivalue && ava.len() > 1 {
|
||||
// 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()));
|
||||
};
|
||||
// If syntax, check the type is correct
|
||||
|
@ -245,11 +255,13 @@ impl SchemaAttribute {
|
|||
SyntaxType::DateTime => ava.is_datetime(),
|
||||
SyntaxType::EmailAddress => ava.is_email_address(),
|
||||
SyntaxType::Url => ava.is_url(),
|
||||
SyntaxType::OauthScope => ava.is_oauthscope(),
|
||||
SyntaxType::OauthScopeMap => ava.is_oauthscopemap(),
|
||||
};
|
||||
if valid {
|
||||
Ok(())
|
||||
} else {
|
||||
trace!(?a, "InvalidAttributeSyntax");
|
||||
admin_error!(?a, "validate_ava - InvalidAttributeSyntax");
|
||||
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.
|
||||
attributetypes.into_iter().for_each(|a| {
|
||||
// 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());
|
||||
}
|
||||
if a.unique {
|
||||
|
@ -1837,6 +1849,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_schema_entries() {
|
||||
let _ = crate::tracing_tree::test_init();
|
||||
// Given an entry, assert it's schema is valid
|
||||
// We do
|
||||
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::Url => Value::new_url_s(value)
|
||||
.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 => {
|
||||
|
@ -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 => {
|
||||
PartialValue::new_json_filter_s(value).ok_or_else(|| {
|
||||
OperationError::InvalidAttribute("Invalid Filter syntax".to_string())
|
||||
|
@ -620,6 +640,7 @@ pub trait QueryServerTransaction<'a> {
|
|||
"Invalid Url (whatwg/url) syntax".to_string(),
|
||||
)
|
||||
}),
|
||||
SyntaxType::OauthScope => Ok(PartialValue::new_oauthscope(value)),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
@ -644,6 +665,19 @@ pub trait QueryServerTransaction<'a> {
|
|||
})
|
||||
.collect();
|
||||
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 {
|
||||
let v: Vec<_> = value.to_proto_string_clone_iter().collect();
|
||||
Ok(v)
|
||||
|
@ -2049,7 +2083,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
//
|
||||
// NOTE: gen modlist IS schema aware and will handle multivalue
|
||||
// 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")]) {
|
||||
Some(f) => f,
|
||||
|
@ -2189,9 +2223,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_SCHEMA_ATTR_ACCOUNT_VALID_FROM,
|
||||
JSON_SCHEMA_ATTR_OAUTH2_RS_NAME,
|
||||
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_TOKEN_KEY,
|
||||
JSON_SCHEMA_ATTR_OAUTH2_RS_TOKEN_KEY,
|
||||
JSON_SCHEMA_CLASS_PERSON,
|
||||
JSON_SCHEMA_CLASS_GROUP,
|
||||
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).
|
||||
|
||||
use crate::be::dbvalue::{
|
||||
DbCidV1, DbValueCredV1, DbValueEmailAddressV1, DbValueTaggedStringV1, DbValueV1,
|
||||
DbCidV1, DbValueCredV1, DbValueEmailAddressV1, DbValueOauthScopeMapV1, DbValueTaggedStringV1,
|
||||
DbValueV1,
|
||||
};
|
||||
use crate::credential::Credential;
|
||||
use crate::repl::cid::Cid;
|
||||
use kanidm_proto::v1::Filter as ProtoFilter;
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::BTreeSet;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
@ -43,6 +45,11 @@ lazy_static! {
|
|||
#[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")
|
||||
};
|
||||
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)]
|
||||
|
@ -136,6 +143,8 @@ pub enum SyntaxType {
|
|||
DateTime,
|
||||
EmailAddress,
|
||||
Url,
|
||||
OauthScope,
|
||||
OauthScopeMap,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for SyntaxType {
|
||||
|
@ -164,6 +173,8 @@ impl TryFrom<&str> for SyntaxType {
|
|||
"DATETIME" => Ok(SyntaxType::DateTime),
|
||||
"EMAIL_ADDRESS" => Ok(SyntaxType::EmailAddress),
|
||||
"URL" => Ok(SyntaxType::Url),
|
||||
"OAUTH_SCOPE" => Ok(SyntaxType::OauthScope),
|
||||
"OAUTH_SCOPE_MAP" => Ok(SyntaxType::OauthScopeMap),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -193,6 +204,8 @@ impl TryFrom<usize> for SyntaxType {
|
|||
16 => Ok(SyntaxType::DateTime),
|
||||
17 => Ok(SyntaxType::EmailAddress),
|
||||
18 => Ok(SyntaxType::Url),
|
||||
19 => Ok(SyntaxType::OauthScope),
|
||||
20 => Ok(SyntaxType::OauthScopeMap),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -220,6 +233,8 @@ impl SyntaxType {
|
|||
SyntaxType::DateTime => 16,
|
||||
SyntaxType::EmailAddress => 17,
|
||||
SyntaxType::Url => 18,
|
||||
SyntaxType::OauthScope => 19,
|
||||
SyntaxType::OauthScopeMap => 20,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -246,6 +261,8 @@ impl fmt::Display for SyntaxType {
|
|||
SyntaxType::DateTime => "DATETIME",
|
||||
SyntaxType::EmailAddress => "EMAIL_ADDRESS",
|
||||
SyntaxType::Url => "URL",
|
||||
SyntaxType::OauthScope => "OAUTH_SCOPE",
|
||||
SyntaxType::OauthScopeMap => "OAUTH_SCOPE_MAP",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -255,6 +272,7 @@ pub enum DataValue {
|
|||
Cred(Credential),
|
||||
SshKey(String),
|
||||
SecretValue(String),
|
||||
OauthScopeMap(BTreeSet<String>),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DataValue {
|
||||
|
@ -263,6 +281,7 @@ impl std::fmt::Debug for DataValue {
|
|||
DataValue::Cred(_) => write!(f, "DataValue::Cred(_)"),
|
||||
DataValue::SshKey(_) => write!(f, "DataValue::SshKey(_)"),
|
||||
DataValue::SecretValue(_) => write!(f, "DataValue::SecretValue(_)"),
|
||||
DataValue::OauthScopeMap(_) => write!(f, "DataValue::OauthScopeMap(_)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -297,6 +316,8 @@ pub enum PartialValue {
|
|||
DateTime(OffsetDateTime),
|
||||
EmailAddress(String),
|
||||
Url(Url),
|
||||
OauthScope(String),
|
||||
OauthScopeMap(Uuid),
|
||||
}
|
||||
|
||||
impl From<SyntaxType> for PartialValue {
|
||||
|
@ -590,6 +611,29 @@ impl PartialValue {
|
|||
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> {
|
||||
match self {
|
||||
PartialValue::Utf8(s) => Some(s.as_str()),
|
||||
|
@ -652,6 +696,8 @@ impl PartialValue {
|
|||
odt.format(time::Format::Rfc3339)
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
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 {
|
||||
self.pv.lessthan(s)
|
||||
}
|
||||
|
@ -1294,6 +1362,16 @@ impl Value {
|
|||
pv: PartialValue::Url(u),
|
||||
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() })
|
||||
}
|
||||
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> {
|
||||
match &self.pv {
|
||||
PartialValue::Refer(u) => Some(u),
|
||||
PartialValue::OauthScopeMap(u) => Some(u),
|
||||
_ => 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> {
|
||||
match self.pv {
|
||||
PartialValue::Iutf8(v) => Some(Value {
|
||||
|
@ -1580,6 +1684,16 @@ impl Value {
|
|||
odt.format(time::Format::Rfc3339)
|
||||
}
|
||||
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::EmailAddress(mail) => validator::validate_email(mail.as_str()),
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
@ -1629,7 +1751,7 @@ impl Value {
|
|||
| PartialValue::Iname(s)
|
||||
| PartialValue::Nsuniqueid(s)
|
||||
| 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()]
|
||||
}
|
||||
PartialValue::Bool(b) => vec![b.to_string()],
|
||||
|
@ -1651,6 +1773,7 @@ impl Value {
|
|||
vec![odt.format(time::Format::Rfc3339)]
|
||||
}
|
||||
PartialValue::Url(u) => vec![u.to_string()],
|
||||
PartialValue::OauthScope(_) => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,8 @@ use time::OffsetDateTime;
|
|||
use tracing::trace;
|
||||
|
||||
use crate::be::dbvalue::{
|
||||
DbCidV1, DbValueCredV1, DbValueEmailAddressV1, DbValueTaggedStringV1, DbValueV1,
|
||||
DbCidV1, DbValueCredV1, DbValueEmailAddressV1, DbValueOauthScopeMapV1, DbValueTaggedStringV1,
|
||||
DbValueV1,
|
||||
};
|
||||
use crate::value::DataValue;
|
||||
|
||||
|
@ -22,7 +23,6 @@ use crate::value::DataValue;
|
|||
enum I {
|
||||
Utf8(BTreeSet<String>),
|
||||
Iutf8(BTreeSet<String>),
|
||||
// Could be AttrString?
|
||||
Iname(BTreeSet<String>),
|
||||
Uuid(BTreeSet<Uuid>),
|
||||
Bool(SmolSet<[bool; 1]>),
|
||||
|
@ -40,6 +40,8 @@ enum I {
|
|||
DateTime(SmolSet<[OffsetDateTime; 1]>),
|
||||
EmailAddress(BTreeSet<String>),
|
||||
Url(SmolSet<[Url; 1]>),
|
||||
OauthScope(BTreeSet<String>),
|
||||
OauthScopeMap(BTreeMap<Uuid, BTreeSet<String>>),
|
||||
}
|
||||
|
||||
pub struct ValueSet {
|
||||
|
@ -110,6 +112,11 @@ impl ValueSet {
|
|||
PartialValue::DateTime(dt) => I::DateTime(smolset![dt]),
|
||||
PartialValue::EmailAddress(e) => I::EmailAddress(btreeset![e]),
|
||||
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::EmailAddress(set), PartialValue::EmailAddress(e)) => Ok(set.insert(e)),
|
||||
(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),
|
||||
}
|
||||
}
|
||||
|
@ -242,6 +263,12 @@ impl ValueSet {
|
|||
(I::Url(a), I::Url(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
|
||||
// type and we need to potentially purge everything, so we just return the left side.
|
||||
_ => Err(OperationError::InvalidValueState),
|
||||
|
@ -328,6 +355,12 @@ impl ValueSet {
|
|||
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) => {
|
||||
set.clear();
|
||||
}
|
||||
I::OauthScope(set) => {
|
||||
set.clear();
|
||||
}
|
||||
I::OauthScopeMap(map) => {
|
||||
map.clear();
|
||||
}
|
||||
};
|
||||
debug_assert!(self.is_empty());
|
||||
}
|
||||
|
@ -454,6 +493,13 @@ impl ValueSet {
|
|||
(I::Url(set), PartialValue::Url(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)
|
||||
}
|
||||
|
@ -484,6 +530,9 @@ impl ValueSet {
|
|||
(I::DateTime(set), PartialValue::DateTime(dt)) => set.contains(dt),
|
||||
(I::EmailAddress(set), PartialValue::EmailAddress(e)) => set.contains(e.as_str()),
|
||||
(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,
|
||||
}
|
||||
}
|
||||
|
@ -526,6 +575,8 @@ impl ValueSet {
|
|||
I::DateTime(set) => set.len(),
|
||||
I::EmailAddress(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(),
|
||||
// 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(),
|
||||
// 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) })
|
||||
}
|
||||
}
|
||||
(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
|
||||
// type and we need to potentially purge everything, so we just return the left side.
|
||||
_ => Some(self.clone()),
|
||||
|
@ -858,6 +940,17 @@ impl ValueSet {
|
|||
.map(|s| s.as_str())
|
||||
.map(Value::new_email_address_s),
|
||||
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> {
|
||||
match &self.inner {
|
||||
I::Bool(set) => {
|
||||
|
@ -1022,9 +1128,10 @@ impl ValueSet {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
I::Refer(set) => Some(set.iter()),
|
||||
I::Refer(set) => Some(Box::new(set.iter())),
|
||||
I::OauthScopeMap(map) => Some(Box::new(map.keys())),
|
||||
_ => 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<'_> {
|
||||
// to_proto_string_clone
|
||||
match &self.inner {
|
||||
|
@ -1058,6 +1179,8 @@ impl ValueSet {
|
|||
I::DateTime(set) => ProtoIter::DateTime(set.iter()),
|
||||
I::EmailAddress(set) => ProtoIter::EmailAddress(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::EmailAddress(set) => DbValueV1Iter::EmailAddress(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::EmailAddress(set) => PartialValueIter::EmailAddress(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::EmailAddress(set) => ValueIter::EmailAddress(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(_))
|
||||
}
|
||||
|
||||
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> {
|
||||
// Swap iutf8 to Iname internally.
|
||||
let ninner = match &self.inner {
|
||||
|
@ -1269,6 +1406,8 @@ impl PartialEq for ValueSet {
|
|||
(I::DateTime(a), I::DateTime(b)) => a.eq(b),
|
||||
(I::EmailAddress(a), I::EmailAddress(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,
|
||||
}
|
||||
}
|
||||
|
@ -1329,6 +1468,8 @@ pub enum ValueIter<'a> {
|
|||
DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>),
|
||||
EmailAddress(std::collections::btree_set::Iter<'a, String>),
|
||||
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> {
|
||||
|
@ -1372,6 +1513,10 @@ impl<'a> Iterator for ValueIter<'a> {
|
|||
iter.next().map(|i| Value::new_email_address_s(i.as_str()))
|
||||
}
|
||||
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]>),
|
||||
EmailAddress(std::collections::btree_set::Iter<'a, String>),
|
||||
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> {
|
||||
|
@ -1447,6 +1594,12 @@ impl<'a> Iterator for PartialValueIter<'a> {
|
|||
.next()
|
||||
.map(|i| PartialValue::new_email_address_s(i.as_str())),
|
||||
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]>),
|
||||
EmailAddress(std::collections::btree_set::Iter<'a, String>),
|
||||
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> {
|
||||
|
@ -1529,6 +1684,15 @@ impl<'a> Iterator for DbValueV1Iter<'a> {
|
|||
.next()
|
||||
.map(|i| DbValueV1::EmailAddress(DbValueEmailAddressV1 { d: 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]>),
|
||||
EmailAddress(std::collections::btree_set::Iter<'a, String>),
|
||||
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> {
|
||||
|
@ -1600,6 +1766,10 @@ impl<'a> Iterator for ProtoIter<'a> {
|
|||
}),
|
||||
ProtoIter::EmailAddress(iter) => iter.next().cloned(),
|
||||
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