509 oauth2 scope mapping (#586)

This commit is contained in:
Firstyear 2021-10-07 18:31:48 +10:00 committed by GitHub
parent d59ddcc74a
commit c62b39c338
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1303 additions and 152 deletions

View file

@ -13,7 +13,6 @@ help:
buildx/kanidmd/simd: ## build multiarch server images buildx/kanidmd/simd: ## build multiarch server images
buildx/kanidmd/simd: buildx/kanidmd/simd:
@docker buildx build $(EXT_OPTS) --pull --push --platform "linux/amd64" \ @docker buildx build $(EXT_OPTS) --pull --push --platform "linux/amd64" \
--allow security.insecure \
-f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:x86_64_$(IMAGE_VERSION) \ -f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:x86_64_$(IMAGE_VERSION) \
--build-arg "KANIDM_BUILD_PROFILE=container_x86_64_v3" \ --build-arg "KANIDM_BUILD_PROFILE=container_x86_64_v3" \
--build-arg "KANIDM_FEATURES=simd_support" \ --build-arg "KANIDM_FEATURES=simd_support" \
@ -23,7 +22,6 @@ buildx/kanidmd/simd:
buildx/kanidmd/x86_64_v3: ## build multiarch server images buildx/kanidmd/x86_64_v3: ## build multiarch server images
buildx/kanidmd/x86_64_v3: buildx/kanidmd/x86_64_v3:
@docker buildx build $(EXT_OPTS) --pull --push --platform "linux/amd64" \ @docker buildx build $(EXT_OPTS) --pull --push --platform "linux/amd64" \
--allow security.insecure \
-f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:x86_64_$(IMAGE_VERSION) \ -f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:x86_64_$(IMAGE_VERSION) \
--build-arg "KANIDM_BUILD_PROFILE=container_x86_64_v3" \ --build-arg "KANIDM_BUILD_PROFILE=container_x86_64_v3" \
--build-arg "KANIDM_FEATURES=" \ --build-arg "KANIDM_FEATURES=" \
@ -33,7 +31,6 @@ buildx/kanidmd/x86_64_v3:
buildx/kanidmd: ## build multiarch server images buildx/kanidmd: ## build multiarch server images
buildx/kanidmd: buildx/kanidmd:
@docker buildx build $(EXT_OPTS) --pull --push --platform $(IMAGE_ARCH) \ @docker buildx build $(EXT_OPTS) --pull --push --platform $(IMAGE_ARCH) \
--allow security.insecure \
-f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:$(IMAGE_VERSION) \ -f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:$(IMAGE_VERSION) \
--build-arg "KANIDM_BUILD_PROFILE=container_generic" \ --build-arg "KANIDM_BUILD_PROFILE=container_generic" \
--build-arg "KANIDM_FEATURES=" \ --build-arg "KANIDM_FEATURES=" \

View file

@ -15,7 +15,8 @@
- [POSIX Accounts and Groups](./posix_accounts.md) - [POSIX Accounts and Groups](./posix_accounts.md)
- [SSH Key Distribution](./ssh_key_dist.md) - [SSH Key Distribution](./ssh_key_dist.md)
- [The Recycle Bin](./recycle_bin.md) - [The Recycle Bin](./recycle_bin.md)
- [LDAP](./ldap.md) - [Oauth2](./oauth2.md)
- [PAM and nsswitch](./pam_and_nsswitch.md) - [PAM and nsswitch](./pam_and_nsswitch.md)
- [RADIUS](./radius.md) - [RADIUS](./radius.md)
- [LDAP](./ldap.md)
- [Why TLS?](./why_tls.md) - [Why TLS?](./why_tls.md)

View file

@ -4,7 +4,7 @@ The monitoring design of Kanidm is still very much in its infancy - [take part i
## kanidmd ## kanidmd
kanidmd currently responds to HTTP GET requests at the `/status` endpoint with a JSON object of either "true" or "false". `true` indicates that the platform is responding to requests. kanidmd currently responds to HTTP GET requests at the `/status` endpoint with a JSON object of either "true" or "false". `true` indicates that the platform is responding to requests.
| URL | `<hostname>/status` | | URL | `<hostname>/status` |
| --- | --- | | --- | --- |

136
kanidm_book/src/oauth2.md Normal file
View 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.

View file

@ -40,9 +40,10 @@ An entry can be revived with:
The recycle bin is a best effort to restore your data - there are some cases where The recycle bin is a best effort to restore your data - there are some cases where
the revived entries may not be the same as their were when they were deleted. This the revived entries may not be the same as their were when they were deleted. This
generally revolves around reference types such as group membership. generally revolves around reference types such as group membership, or when the reference
type includes supplemental map data such as the oauth2 scope map type.
An example of this is the following steps: An example of this data loss is the following steps:
add user1 add user1
add group1 add group1
@ -63,7 +64,7 @@ membership of user1 in group1 would be lost in this process. To explain why:
revive user1 // re-add groups based on directmemberof (empty set) revive user1 // re-add groups based on directmemberof (empty set)
revive group1 // no members revive group1 // no members
This issue could be looked at again in the future, but for now we think that deletes of These issues could be looked at again in the future, but for now we think that deletes of
groups is rare - we expect recycle bin to save you in "opps" moments, and in a majority groups is rare - we expect recycle bin to save you in "opps" moments, and in a majority
of cases you may delete a group or a user and then restore them. To handle this series of cases you may delete a group or a user and then restore them. To handle this series
of steps requires extra code complexity in how we flag operations. For more, of steps requires extra code complexity in how we flag operations. For more,

View file

@ -1378,12 +1378,16 @@ impl KanidmAsyncClient {
pub async fn idm_oauth2_rs_basic_create( pub async fn idm_oauth2_rs_basic_create(
&self, &self,
name: &str, name: &str,
displayname: &str,
origin: &str, origin: &str,
) -> Result<(), ClientError> { ) -> Result<(), ClientError> {
let mut new_oauth2_rs = Entry::default(); let mut new_oauth2_rs = Entry::default();
new_oauth2_rs new_oauth2_rs
.attrs .attrs
.insert("oauth2_rs_name".to_string(), vec![name.to_string()]); .insert("oauth2_rs_name".to_string(), vec![name.to_string()]);
new_oauth2_rs
.attrs
.insert("displayname".to_string(), vec![displayname.to_string()]);
new_oauth2_rs new_oauth2_rs
.attrs .attrs
.insert("oauth2_rs_origin".to_string(), vec![origin.to_string()]); .insert("oauth2_rs_origin".to_string(), vec![origin.to_string()]);
@ -1400,7 +1404,9 @@ impl KanidmAsyncClient {
&self, &self,
id: &str, id: &str,
name: Option<&str>, name: Option<&str>,
displayname: Option<&str>,
origin: Option<&str>, origin: Option<&str>,
scopes: Option<Vec<&str>>,
reset_secret: bool, reset_secret: bool,
reset_token_key: bool, reset_token_key: bool,
) -> Result<(), ClientError> { ) -> Result<(), ClientError> {
@ -1413,11 +1419,22 @@ impl KanidmAsyncClient {
.attrs .attrs
.insert("oauth2_rs_name".to_string(), vec![newname.to_string()]); .insert("oauth2_rs_name".to_string(), vec![newname.to_string()]);
} }
if let Some(newdisplayname) = displayname {
update_oauth2_rs
.attrs
.insert("displayname".to_string(), vec![newdisplayname.to_string()]);
}
if let Some(neworigin) = origin { if let Some(neworigin) = origin {
update_oauth2_rs update_oauth2_rs
.attrs .attrs
.insert("oauth2_rs_origin".to_string(), vec![neworigin.to_string()]); .insert("oauth2_rs_origin".to_string(), vec![neworigin.to_string()]);
} }
if let Some(newscopes) = scopes {
update_oauth2_rs.attrs.insert(
"oauth2_rs_implicit_scopes".to_string(),
newscopes.into_iter().map(str::to_string).collect(),
);
}
if reset_secret { if reset_secret {
update_oauth2_rs update_oauth2_rs
.attrs .attrs
@ -1426,13 +1443,36 @@ impl KanidmAsyncClient {
if reset_token_key { if reset_token_key {
update_oauth2_rs update_oauth2_rs
.attrs .attrs
.insert("oauth2_rs_basic_token_key".to_string(), Vec::new()); .insert("oauth2_rs_token_key".to_string(), Vec::new());
} }
self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
.await .await
} }
pub async fn idm_oauth2_rs_create_scope_map(
&self,
id: &str,
group: &str,
scopes: Vec<&str>,
) -> Result<(), ClientError> {
let scopes: Vec<String> = scopes.into_iter().map(str::to_string).collect();
self.perform_post_request(
format!("/v1/oauth2/{}/_scopemap/{}", id, group).as_str(),
scopes,
)
.await
}
pub async fn idm_oauth2_rs_delete_scope_map(
&self,
id: &str,
group: &str,
) -> Result<(), ClientError> {
self.perform_delete_request(format!("/v1/oauth2/{}/_scopemap/{}", id, group).as_str())
.await
}
pub async fn idm_oauth2_rs_delete(&self, id: &str) -> Result<(), ClientError> { pub async fn idm_oauth2_rs_delete(&self, id: &str) -> Result<(), ClientError> {
self.perform_delete_request(["/v1/oauth2/", id].concat().as_str()) self.perform_delete_request(["/v1/oauth2/", id].concat().as_str())
.await .await

View file

@ -891,8 +891,16 @@ impl KanidmClient {
tokio_block_on(self.asclient.idm_oauth2_rs_list()) tokio_block_on(self.asclient.idm_oauth2_rs_list())
} }
pub fn idm_oauth2_rs_basic_create(&self, name: &str, origin: &str) -> Result<(), ClientError> { pub fn idm_oauth2_rs_basic_create(
tokio_block_on(self.asclient.idm_oauth2_rs_basic_create(name, origin)) &self,
name: &str,
displayname: &str,
origin: &str,
) -> Result<(), ClientError> {
tokio_block_on(
self.asclient
.idm_oauth2_rs_basic_create(name, displayname, origin),
)
} }
pub fn idm_oauth2_rs_get(&self, id: &str) -> Result<Option<Entry>, ClientError> { pub fn idm_oauth2_rs_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
@ -903,19 +911,39 @@ impl KanidmClient {
&self, &self,
id: &str, id: &str,
name: Option<&str>, name: Option<&str>,
displayname: Option<&str>,
origin: Option<&str>, origin: Option<&str>,
scopes: Option<Vec<&str>>,
reset_secret: bool, reset_secret: bool,
reset_token_key: bool, reset_token_key: bool,
) -> Result<(), ClientError> { ) -> Result<(), ClientError> {
tokio_block_on(self.asclient.idm_oauth2_rs_update( tokio_block_on(self.asclient.idm_oauth2_rs_update(
id, id,
name, name,
displayname,
origin, origin,
scopes,
reset_secret, reset_secret,
reset_token_key, reset_token_key,
)) ))
} }
pub fn idm_oauth2_rs_create_scope_map(
&self,
id: &str,
group: &str,
scopes: Vec<&str>,
) -> Result<(), ClientError> {
tokio_block_on(
self.asclient
.idm_oauth2_rs_create_scope_map(id, group, scopes),
)
}
pub fn idm_oauth2_rs_delete_scope_map(&self, id: &str, group: &str) -> Result<(), ClientError> {
tokio_block_on(self.asclient.idm_oauth2_rs_delete_scope_map(id, group))
}
pub fn idm_oauth2_rs_delete(&self, id: &str) -> Result<(), ClientError> { pub fn idm_oauth2_rs_delete(&self, id: &str) -> Result<(), ClientError> {
tokio_block_on(self.asclient.idm_oauth2_rs_delete(id)) tokio_block_on(self.asclient.idm_oauth2_rs_delete(id))
} }

View file

@ -39,9 +39,25 @@ fn test_oauth2_basic_flow() {
// Create an oauth2 application integration. // Create an oauth2 application integration.
rsclient rsclient
.idm_oauth2_rs_basic_create("test_integration", "https://demo.example.com") .idm_oauth2_rs_basic_create(
"test_integration",
"Test Integration",
"https://demo.example.com",
)
.expect("Failed to create oauth2 config"); .expect("Failed to create oauth2 config");
rsclient
.idm_oauth2_rs_update(
"test_integration",
None,
None,
None,
Some(vec!["read", "email"]),
false,
false,
)
.expect("Failed to update oauth2 config");
let oauth2_config = rsclient let oauth2_config = rsclient
.idm_oauth2_rs_get("test_integration") .idm_oauth2_rs_get("test_integration")
.ok() .ok()
@ -90,7 +106,7 @@ fn test_oauth2_basic_flow() {
("code_challenge", pkce_code_challenge.as_str()), ("code_challenge", pkce_code_challenge.as_str()),
("code_challenge_method", "S256"), ("code_challenge_method", "S256"),
("redirect_uri", "https://demo.example.com/oauth2/flow"), ("redirect_uri", "https://demo.example.com/oauth2/flow"),
("scope", "mail+name+test"), ("scope", "email read"),
]) ])
.send() .send()
.await .await

View file

@ -1073,7 +1073,11 @@ fn test_server_rest_oauth2_basic_lifecycle() {
// Create a new oauth2 config // Create a new oauth2 config
rsclient rsclient
.idm_oauth2_rs_basic_create("test_integration", "https://demo.example.com") .idm_oauth2_rs_basic_create(
"test_integration",
"Test Integration",
"https://demo.example.com",
)
.expect("Failed to create oauth2 config"); .expect("Failed to create oauth2 config");
// List, there is what we created. // List, there is what we created.
@ -1091,12 +1095,12 @@ fn test_server_rest_oauth2_basic_lifecycle() {
.flatten() .flatten()
.expect("Failed to retrieve test_integration config"); .expect("Failed to retrieve test_integration config");
eprintln!("{:?}", oauth2_config);
// What can we see? // What can we see?
assert!(oauth2_config.attrs.contains_key("oauth2_rs_basic_secret")); assert!(oauth2_config.attrs.contains_key("oauth2_rs_basic_secret"));
// This is present, but redacted. // This is present, but redacted.
assert!(oauth2_config assert!(oauth2_config.attrs.contains_key("oauth2_rs_token_key"));
.attrs
.contains_key("oauth2_rs_basic_token_key"));
// Mod delete the secret/key and check them again. // Mod delete the secret/key and check them again.
// Check we can patch the oauth2_rs_name / oauth2_rs_origin // Check we can patch the oauth2_rs_name / oauth2_rs_origin
@ -1104,7 +1108,9 @@ fn test_server_rest_oauth2_basic_lifecycle() {
.idm_oauth2_rs_update( .idm_oauth2_rs_update(
"test_integration", "test_integration",
None, None,
Some("Test Integration"),
Some("https://new_demo.example.com"), Some("https://new_demo.example.com"),
Some(vec!["read", "email"]),
true, true,
true, true,
) )
@ -1118,6 +1124,34 @@ fn test_server_rest_oauth2_basic_lifecycle() {
assert!(oauth2_config_updated != oauth2_config); assert!(oauth2_config_updated != oauth2_config);
// Check that we can add scope maps and delete them.
rsclient
.idm_oauth2_rs_create_scope_map("test_integration", "system_admins", vec!["a", "b"])
.expect("Failed to create scope map");
let oauth2_config_updated2 = rsclient
.idm_oauth2_rs_get("test_integration")
.ok()
.flatten()
.expect("Failed to retrieve test_integration config");
assert!(oauth2_config_updated != oauth2_config_updated2);
rsclient
.idm_oauth2_rs_delete_scope_map("test_integration", "system_admins")
.expect("Failed to delete scope map");
let oauth2_config_updated3 = rsclient
.idm_oauth2_rs_get("test_integration")
.ok()
.flatten()
.expect("Failed to retrieve test_integration config");
eprintln!("{:?}", oauth2_config_updated);
eprintln!("{:?}", oauth2_config_updated3);
assert!(oauth2_config_updated == oauth2_config_updated3);
// Delete the config // Delete the config
rsclient rsclient
.idm_oauth2_rs_delete("test_integration") .idm_oauth2_rs_delete("test_integration")

View file

@ -31,6 +31,7 @@ pub struct AuthorisationRequest {
pub struct ConsentRequest { pub struct ConsentRequest {
// A pretty-name of the client // A pretty-name of the client
pub client_name: String, pub client_name: String,
// A list of scopes requested / to be issued.
pub scopes: Vec<String>, pub scopes: Vec<String>,
// The users displayname (?) // The users displayname (?)
// pub display_name: String, // pub display_name: String,

View file

@ -6,6 +6,10 @@ impl Oauth2Opt {
Oauth2Opt::List(copt) => copt.debug, Oauth2Opt::List(copt) => copt.debug,
Oauth2Opt::Get(nopt) => nopt.copt.debug, Oauth2Opt::Get(nopt) => nopt.copt.debug,
Oauth2Opt::CreateBasic(cbopt) => cbopt.nopt.copt.debug, Oauth2Opt::CreateBasic(cbopt) => cbopt.nopt.copt.debug,
Oauth2Opt::SetImplictScopes(cbopt) => cbopt.nopt.copt.debug,
Oauth2Opt::CreateScopeMap(cbopt) => cbopt.nopt.copt.debug,
Oauth2Opt::DeleteScopeMap(cbopt) => cbopt.nopt.copt.debug,
Oauth2Opt::ResetSecrets(cbopt) => cbopt.copt.debug,
Oauth2Opt::Delete(nopt) => nopt.copt.debug, Oauth2Opt::Delete(nopt) => nopt.copt.debug,
} }
} }
@ -28,14 +32,66 @@ impl Oauth2Opt {
} }
} }
Oauth2Opt::CreateBasic(cbopt) => { Oauth2Opt::CreateBasic(cbopt) => {
let client = cbopt.nopt.copt.to_client();
match client.idm_oauth2_rs_basic_create(
cbopt.nopt.name.as_str(),
cbopt.displayname.as_str(),
cbopt.origin.as_str(),
) {
Ok(_) => println!("Success"),
Err(e) => eprintln!("Error -> {:?}", e),
}
}
Oauth2Opt::SetImplictScopes(cbopt) => {
let client = cbopt.nopt.copt.to_client();
match client.idm_oauth2_rs_update(
cbopt.nopt.name.as_str(),
None,
None,
None,
Some(cbopt.scopes.iter().map(|s| s.as_str()).collect()),
false,
false,
) {
Ok(_) => println!("Success"),
Err(e) => eprintln!("Error -> {:?}", e),
}
}
Oauth2Opt::CreateScopeMap(cbopt) => {
let client = cbopt.nopt.copt.to_client();
match client.idm_oauth2_rs_create_scope_map(
cbopt.nopt.name.as_str(),
cbopt.group.as_str(),
cbopt.scopes.iter().map(|s| s.as_str()).collect(),
) {
Ok(_) => println!("Success"),
Err(e) => eprintln!("Error -> {:?}", e),
}
}
Oauth2Opt::DeleteScopeMap(cbopt) => {
let client = cbopt.nopt.copt.to_client(); let client = cbopt.nopt.copt.to_client();
match client match client
.idm_oauth2_rs_basic_create(cbopt.nopt.name.as_str(), cbopt.origin.as_str()) .idm_oauth2_rs_delete_scope_map(cbopt.nopt.name.as_str(), cbopt.group.as_str())
{ {
Ok(_) => println!("Success"), Ok(_) => println!("Success"),
Err(e) => eprintln!("Error -> {:?}", e), Err(e) => eprintln!("Error -> {:?}", e),
} }
} }
Oauth2Opt::ResetSecrets(cbopt) => {
let client = cbopt.copt.to_client();
match client.idm_oauth2_rs_update(
cbopt.name.as_str(),
None,
None,
None,
None,
true,
true,
) {
Ok(_) => println!("Success"),
Err(e) => eprintln!("Error -> {:?}", e),
}
}
Oauth2Opt::Delete(nopt) => { Oauth2Opt::Delete(nopt) => {
let client = nopt.copt.to_client(); let client = nopt.copt.to_client();
match client.idm_oauth2_rs_delete(nopt.name.as_str()) { match client.idm_oauth2_rs_delete(nopt.name.as_str()) {

View file

@ -354,10 +354,38 @@ pub enum SelfOpt {
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
pub struct Oauth2BasicCreateOpt { pub struct Oauth2BasicCreateOpt {
#[structopt(name = "origin")]
origin: String,
#[structopt(flatten)] #[structopt(flatten)]
nopt: Named, nopt: Named,
#[structopt(name = "displayname")]
displayname: String,
#[structopt(name = "origin")]
origin: String,
}
#[derive(Debug, StructOpt)]
pub struct Oauth2SetImplicitScopes {
#[structopt(flatten)]
nopt: Named,
#[structopt(name = "scopes")]
scopes: Vec<String>,
}
#[derive(Debug, StructOpt)]
pub struct Oauth2CreateScopeMapOpt {
#[structopt(flatten)]
nopt: Named,
#[structopt(name = "group")]
group: String,
#[structopt(name = "scopes")]
scopes: Vec<String>,
}
#[derive(Debug, StructOpt)]
pub struct Oauth2DeleteScopeMapOpt {
#[structopt(flatten)]
nopt: Named,
#[structopt(name = "group")]
group: String,
} }
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
@ -374,6 +402,18 @@ pub enum Oauth2Opt {
#[structopt(name = "create")] #[structopt(name = "create")]
/// Create a new oauth2 resource server /// Create a new oauth2 resource server
CreateBasic(Oauth2BasicCreateOpt), CreateBasic(Oauth2BasicCreateOpt),
#[structopt(name = "set_implicit_scopes")]
/// Set the list of scopes that are granted to all valid accounts.
SetImplictScopes(Oauth2SetImplicitScopes),
#[structopt(name = "create_scope_map")]
/// Add a new mapping from a group to what scopes it provides
CreateScopeMap(Oauth2CreateScopeMapOpt),
#[structopt(name = "delete_scope_map")]
/// Remove a mapping from groups to scopes
DeleteScopeMap(Oauth2DeleteScopeMapOpt),
#[structopt(name = "reset_secrets")]
/// Reset the secrets associated to this resource server
ResetSecrets(Named),
#[structopt(name = "delete")] #[structopt(name = "delete")]
/// Delete a oauth2 resource server /// Delete a oauth2 resource server
Delete(Named), Delete(Named),

View file

@ -1121,6 +1121,129 @@ impl QueryServerWriteV1 {
res res
} }
#[instrument(
level = "trace",
name = "oauth2_scopemap_create",
skip(self, uat, filter, eventid)
fields(uuid = ?eventid)
)]
pub async fn handle_oauth2_scopemap_create(
&self,
uat: Option<String>,
group: String,
scopes: Vec<String>,
filter: Filter<FilterInvalid>,
eventid: Uuid,
) -> Result<(), OperationError> {
// Because this is from internal, we can generate a real modlist, rather
// than relying on the proto ones.
let idms_prox_write = self.idms.proxy_write_async(duration_from_epoch_now()).await;
spanned!("handle_oauth2_scopemap_create", {
let ct = duration_from_epoch_now();
let ident = idms_prox_write
.validate_and_parse_uat(uat.as_deref(), ct)
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
.map_err(|e| {
admin_error!(err = ?e, "Invalid identity");
e
})?;
let group_uuid = idms_prox_write
.qs_write
.name_to_uuid(group.as_str())
.map_err(|e| {
admin_error!(err = ?e, "Error resolving group name to target");
e
})?;
let ml = ModifyList::new_append(
"oauth2_rs_scope_map",
Value::new_oauthscopemap(group_uuid, scopes.into_iter().collect()),
);
let mdf = match ModifyEvent::from_internal_parts(
ident,
&ml,
&filter,
&idms_prox_write.qs_write,
) {
Ok(m) => m,
Err(e) => {
admin_error!(err = ?e, "Failed to begin modify");
return Err(e);
}
};
trace!(?mdf, "Begin modify event");
idms_prox_write
.qs_write
.modify(&mdf)
.and_then(|_| idms_prox_write.commit().map(|_| ()))
})
}
#[instrument(
level = "trace",
name = "oauth2_scopemap_delete",
skip(self, uat, filter, eventid)
fields(uuid = ?eventid)
)]
pub async fn handle_oauth2_scopemap_delete(
&self,
uat: Option<String>,
group: String,
filter: Filter<FilterInvalid>,
eventid: Uuid,
) -> Result<(), OperationError> {
let idms_prox_write = self.idms.proxy_write_async(duration_from_epoch_now()).await;
spanned!("handle_oauth2_scopemap_create", {
let ct = duration_from_epoch_now();
let ident = idms_prox_write
.validate_and_parse_uat(uat.as_deref(), ct)
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
.map_err(|e| {
admin_error!(err = ?e, "Invalid identity");
e
})?;
let group_uuid = idms_prox_write
.qs_write
.name_to_uuid(group.as_str())
.map_err(|e| {
admin_error!(err = ?e, "Error resolving group name to target");
e
})?;
let ml = ModifyList::new_remove(
"oauth2_rs_scope_map",
PartialValue::new_oauthscopemap(group_uuid),
);
let mdf = match ModifyEvent::from_internal_parts(
ident,
&ml,
&filter,
&idms_prox_write.qs_write,
) {
Ok(m) => m,
Err(e) => {
admin_error!(err = ?e, "Failed to begin modify");
return Err(e);
}
};
trace!(?mdf, "Begin modify event");
idms_prox_write
.qs_write
.modify(&mdf)
.and_then(|_| idms_prox_write.commit().map(|_| ()))
})
}
// ===== These below are internal only event types. ===== // ===== These below are internal only event types. =====
#[instrument( #[instrument(
level = "trace", level = "trace",

View file

@ -136,6 +136,14 @@ pub struct DbValueEmailAddressV1 {
pub d: String, pub d: String,
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct DbValueOauthScopeMapV1 {
#[serde(rename = "u")]
pub refer: Uuid,
#[serde(rename = "m")]
pub data: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub enum DbValueV1 { pub enum DbValueV1 {
#[serde(rename = "U8")] #[serde(rename = "U8")]
@ -176,6 +184,10 @@ pub enum DbValueV1 {
EmailAddress(DbValueEmailAddressV1), EmailAddress(DbValueEmailAddressV1),
#[serde(rename = "UR")] #[serde(rename = "UR")]
Url(Url), Url(Url),
#[serde(rename = "OS")]
OauthScope(String),
#[serde(rename = "OM")]
OauthScopeMap(DbValueOauthScopeMapV1),
} }
#[cfg(test)] #[cfg(test)]

View file

@ -1014,33 +1014,41 @@ pub const JSON_IDM_HP_ACP_OAUTH2_MANAGE_PRIV_V1: &str = r#"{
"acp_search_attr": [ "acp_search_attr": [
"class", "class",
"description", "description",
"displayname",
"oauth2_rs_name", "oauth2_rs_name",
"oauth2_rs_origin", "oauth2_rs_origin",
"oauth2_rs_account_filter", "oauth2_rs_scope_map",
"oauth2_rs_implicit_scopes",
"oauth2_rs_basic_secret", "oauth2_rs_basic_secret",
"oauth2_rs_basic_token_key" "oauth2_rs_token_key"
], ],
"acp_modify_removedattr": [ "acp_modify_removedattr": [
"description", "description",
"displayname",
"oauth2_rs_name", "oauth2_rs_name",
"oauth2_rs_origin", "oauth2_rs_origin",
"oauth2_rs_account_filter", "oauth2_rs_scope_map",
"oauth2_rs_implicit_scopes",
"oauth2_rs_basic_secret", "oauth2_rs_basic_secret",
"oauth2_rs_basic_token_key" "oauth2_rs_token_key"
], ],
"acp_modify_presentattr": [ "acp_modify_presentattr": [
"description", "description",
"displayname",
"oauth2_rs_name", "oauth2_rs_name",
"oauth2_rs_origin", "oauth2_rs_origin",
"oauth2_rs_account_filter" "oauth2_rs_scope_map",
"oauth2_rs_implicit_scopes"
], ],
"acp_modify_class": [], "acp_modify_class": [],
"acp_create_attr": [ "acp_create_attr": [
"class", "class",
"description", "description",
"displayname",
"oauth2_rs_name", "oauth2_rs_name",
"oauth2_rs_origin", "oauth2_rs_origin",
"oauth2_rs_account_filter" "oauth2_rs_scope_map",
"oauth2_rs_implicit_scopes"
], ],
"acp_create_class": ["oauth2_resource_server", "oauth2_resource_server_basic", "object"] "acp_create_class": ["oauth2_resource_server", "oauth2_resource_server_basic", "object"]
} }

View file

@ -572,7 +572,7 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_ORIGIN: &str = r#"{
} }
}"#; }"#;
pub const JSON_SCHEMA_ATTR_OAUTH2_RS_ACCOUNT_FILTER: &str = r#"{ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP: &str = r#"{
"attrs": { "attrs": {
"class": [ "class": [
"object", "object",
@ -580,20 +580,22 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_ACCOUNT_FILTER: &str = r#"{
"attributetype" "attributetype"
], ],
"description": [ "description": [
"A filter describing who may access the associated oauth2 resource server" "A reference to a group mapped to scopes for the associated oauth2 resource server"
],
"index": [
"EQUALITY"
], ],
"index": [],
"unique": [ "unique": [
"false" "false"
], ],
"multivalue": [ "multivalue": [
"false" "true"
], ],
"attributename": [ "attributename": [
"oauth2_rs_account_filter" "oauth2_rs_scope_map"
], ],
"syntax": [ "syntax": [
"JSON_FILTER" "OAUTH_SCOPE_MAP"
], ],
"uuid": [ "uuid": [
"00000000-0000-0000-0000-ffff00000082" "00000000-0000-0000-0000-ffff00000082"
@ -630,7 +632,7 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET: &str = r#"{
} }
}"#; }"#;
pub const JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str = r#"{ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_TOKEN_KEY: &str = r#"{
"attrs": { "attrs": {
"class": [ "class": [
"object", "object",
@ -638,7 +640,7 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str = r#"{
"attributetype" "attributetype"
], ],
"description": [ "description": [
"An oauth2 basic resource servers unique token signing key" "An oauth2 resource servers unique token signing key"
], ],
"index": [], "index": [],
"unique": [ "unique": [
@ -648,7 +650,7 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str = r#"{
"false" "false"
], ],
"attributename": [ "attributename": [
"oauth2_rs_basic_token_key" "oauth2_rs_token_key"
], ],
"syntax": [ "syntax": [
"SECRET_UTF8STRING" "SECRET_UTF8STRING"
@ -659,6 +661,35 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str = r#"{
} }
}"#; }"#;
pub const JSON_SCHEMA_ATTR_OAUTH2_RS_IMPLICIT_SCOPES: &str = r#"{
"attrs": {
"class": [
"object",
"system",
"attributetype"
],
"description": [
"An oauth2 resource servers scopes that are implicitly granted to all users"
],
"index": [],
"unique": [
"false"
],
"multivalue": [
"true"
],
"attributename": [
"oauth2_rs_implicit_scopes"
],
"syntax": [
"OAUTH_SCOPE"
],
"uuid": [
"00000000-0000-0000-0000-ffff00000089"
]
}
}"#;
// === classes === // === classes ===
pub const JSON_SCHEMA_CLASS_PERSON: &str = r#" pub const JSON_SCHEMA_CLASS_PERSON: &str = r#"
@ -879,11 +910,14 @@ pub const JSON_SCHEMA_CLASS_OAUTH2_RS: &str = r#"
], ],
"systemmay": [ "systemmay": [
"description", "description",
"oauth2_rs_account_filter" "oauth2_rs_scope_map",
"oauth2_rs_implicit_scopes"
], ],
"systemmust": [ "systemmust": [
"oauth2_rs_name", "oauth2_rs_name",
"oauth2_rs_origin" "displayname",
"oauth2_rs_origin",
"oauth2_rs_token_key"
], ],
"uuid": [ "uuid": [
"00000000-0000-0000-0000-ffff00000085" "00000000-0000-0000-0000-ffff00000085"
@ -901,15 +935,14 @@ pub const JSON_SCHEMA_CLASS_OAUTH2_RS_BASIC: &str = r#"
"classtype" "classtype"
], ],
"description": [ "description": [
"The class representing a configured Oauth2 Resource Server" "The class representing a configured Oauth2 Resource Server authenticated with http basic"
], ],
"classname": [ "classname": [
"oauth2_resource_server_basic" "oauth2_resource_server_basic"
], ],
"systemmay": [], "systemmay": [],
"systemmust": [ "systemmust": [
"oauth2_rs_basic_secret", "oauth2_rs_basic_secret"
"oauth2_rs_basic_token_key"
], ],
"uuid": [ "uuid": [
"00000000-0000-0000-0000-ffff00000086" "00000000-0000-0000-0000-ffff00000086"

View file

@ -22,7 +22,7 @@ pub const _STR_UUID_IDM_GROUP_MANAGE_PRIV: &str = "00000000-0000-0000-0000-00000
pub const _STR_UUID_IDM_HP_ACCOUNT_MANAGE_PRIV: &str = "00000000-0000-0000-0000-000000000016"; pub const _STR_UUID_IDM_HP_ACCOUNT_MANAGE_PRIV: &str = "00000000-0000-0000-0000-000000000016";
pub const _STR_UUID_IDM_HP_GROUP_MANAGE_PRIV: &str = "00000000-0000-0000-0000-000000000017"; pub const _STR_UUID_IDM_HP_GROUP_MANAGE_PRIV: &str = "00000000-0000-0000-0000-000000000017";
pub const STR_UUID_IDM_ADMIN_V1: &str = "00000000-0000-0000-0000-000000000018"; pub const STR_UUID_IDM_ADMIN_V1: &str = "00000000-0000-0000-0000-000000000018";
pub const _STR_UUID_SYSTEM_ADMINS: &str = "00000000-0000-0000-0000-000000000019"; pub const STR_UUID_SYSTEM_ADMINS: &str = "00000000-0000-0000-0000-000000000019";
pub const STR_UUID_DOMAIN_ADMINS: &str = "00000000-0000-0000-0000-000000000020"; pub const STR_UUID_DOMAIN_ADMINS: &str = "00000000-0000-0000-0000-000000000020";
pub const _STR_UUID_IDM_ACCOUNT_UNIX_EXTEND_PRIV: &str = "00000000-0000-0000-0000-000000000021"; pub const _STR_UUID_IDM_ACCOUNT_UNIX_EXTEND_PRIV: &str = "00000000-0000-0000-0000-000000000021";
pub const _STR_UUID_IDM_GROUP_UNIX_EXTEND_PRIV: &str = "00000000-0000-0000-0000-000000000022"; pub const _STR_UUID_IDM_GROUP_UNIX_EXTEND_PRIV: &str = "00000000-0000-0000-0000-000000000022";
@ -134,16 +134,16 @@ pub const STR_UUID_SCHEMA_ATTR_UIDNUMBER: &str = "00000000-0000-0000-0000-ffff00
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_NAME: &str = "00000000-0000-0000-0000-ffff00000080"; pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_NAME: &str = "00000000-0000-0000-0000-ffff00000080";
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_ORIGIN: &str = "00000000-0000-0000-0000-ffff00000081"; pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_ORIGIN: &str = "00000000-0000-0000-0000-ffff00000081";
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_ACCOUNT_FILTER: &str = pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP: &str = "00000000-0000-0000-0000-ffff00000082";
"00000000-0000-0000-0000-ffff00000082";
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET: &str = pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET: &str =
"00000000-0000-0000-0000-ffff00000083"; "00000000-0000-0000-0000-ffff00000083";
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str = pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_TOKEN_KEY: &str = "00000000-0000-0000-0000-ffff00000084";
"00000000-0000-0000-0000-ffff00000084";
pub const STR_UUID_SCHEMA_CLASS_OAUTH2_RS: &str = "00000000-0000-0000-0000-ffff00000085"; pub const STR_UUID_SCHEMA_CLASS_OAUTH2_RS: &str = "00000000-0000-0000-0000-ffff00000085";
pub const STR_UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC: &str = "00000000-0000-0000-0000-ffff00000086"; pub const STR_UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC: &str = "00000000-0000-0000-0000-ffff00000086";
pub const STR_UUID_SCHEMA_ATTR_CN: &str = "00000000-0000-0000-0000-ffff00000087"; pub const STR_UUID_SCHEMA_ATTR_CN: &str = "00000000-0000-0000-0000-ffff00000087";
pub const STR_UUID_SCHEMA_ATTR_DOMAIN_TOKEN_KEY: &str = "00000000-0000-0000-0000-ffff00000088"; pub const STR_UUID_SCHEMA_ATTR_DOMAIN_TOKEN_KEY: &str = "00000000-0000-0000-0000-ffff00000088";
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_IMPLICIT_SCOPES: &str =
"00000000-0000-0000-0000-ffff00000089";
// System and domain infos // System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations. // I'd like to strongly criticise william of the past for making poor choices about these allocations.
@ -201,6 +201,7 @@ pub const STR_UUID_ANONYMOUS: &str = "00000000-0000-0000-0000-ffffffffffff";
lazy_static! { lazy_static! {
pub static ref UUID_ADMIN: Uuid = Uuid::parse_str(STR_UUID_ADMIN).unwrap(); pub static ref UUID_ADMIN: Uuid = Uuid::parse_str(STR_UUID_ADMIN).unwrap();
pub static ref UUID_SYSTEM_ADMINS: Uuid = Uuid::parse_str(STR_UUID_SYSTEM_ADMINS).unwrap();
pub static ref UUID_IDM_ADMIN: Uuid = Uuid::parse_str(STR_UUID_IDM_ADMIN_V1).unwrap(); pub static ref UUID_IDM_ADMIN: Uuid = Uuid::parse_str(STR_UUID_IDM_ADMIN_V1).unwrap();
pub static ref UUID_DOES_NOT_EXIST: Uuid = Uuid::parse_str(STR_UUID_DOES_NOT_EXIST).unwrap(); pub static ref UUID_DOES_NOT_EXIST: Uuid = Uuid::parse_str(STR_UUID_DOES_NOT_EXIST).unwrap();
pub static ref UUID_ANONYMOUS: Uuid = Uuid::parse_str(STR_UUID_ANONYMOUS).unwrap(); pub static ref UUID_ANONYMOUS: Uuid = Uuid::parse_str(STR_UUID_ANONYMOUS).unwrap();

View file

@ -449,6 +449,11 @@ pub fn create_https_server(
.patch(oauth2_id_patch) .patch(oauth2_id_patch)
.delete(oauth2_id_delete); .delete(oauth2_id_delete);
oauth2_route
.at("/:id/_scopemap/:group")
.post(oauth2_id_scopemap_post)
.delete(oauth2_id_scopemap_delete);
let mut self_route = appserver.at("/v1/self"); let mut self_route = appserver.at("/v1/self");
self_route.at("/").get(whoami); self_route.at("/").get(whoami);

View file

@ -69,6 +69,40 @@ pub async fn oauth2_id_patch(mut req: tide::Request<AppState>) -> tide::Result {
to_tide_response(res, hvalue) to_tide_response(res, hvalue)
} }
pub async fn oauth2_id_scopemap_post(mut req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let id = req.get_url_param("id")?;
let group = req.get_url_param("group")?;
let scopes: Vec<String> = req.body_json().await?;
let filter = oauth2_id(&id);
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
.qe_w_ref
.handle_oauth2_scopemap_create(uat, group, scopes, filter, eventid)
.await;
to_tide_response(res, hvalue)
}
pub async fn oauth2_id_scopemap_delete(req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let id = req.get_url_param("id")?;
let group = req.get_url_param("group")?;
let filter = oauth2_id(&id);
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
.qe_w_ref
.handle_oauth2_scopemap_delete(uat, group, filter, eventid)
.await;
to_tide_response(res, hvalue)
}
pub async fn oauth2_id_delete(req: tide::Request<AppState>) -> tide::Result { pub async fn oauth2_id_delete(req: tide::Request<AppState>) -> tide::Result {
// Delete this // Delete this
let uat = req.get_current_uat(); let uat = req.get_current_uat();

View file

@ -1592,6 +1592,19 @@ impl<VALID, STATE> Entry<VALID, STATE> {
self.attrs.get(attr) self.attrs.get(attr)
} }
#[inline(always)]
pub fn get_ava_as_oauthscopes(&self, attr: &str) -> Option<impl Iterator<Item = &str>> {
self.attrs.get(attr).and_then(|vs| vs.as_oauthscope_iter())
}
#[inline(always)]
pub fn get_ava_as_oauthscopemaps(
&self,
attr: &str,
) -> Option<&std::collections::BTreeMap<Uuid, std::collections::BTreeSet<String>>> {
self.attrs.get(attr).and_then(|vs| vs.as_oauthscopemap())
}
#[inline(always)] #[inline(always)]
/// If possible, return an iterator over the set of values transformed into a `&str`. /// If possible, return an iterator over the set of values transformed into a `&str`.
pub fn get_ava_as_str(&self, attr: &str) -> Option<impl Iterator<Item = &str>> { pub fn get_ava_as_str(&self, attr: &str) -> Option<impl Iterator<Item = &str>> {
@ -1600,7 +1613,7 @@ impl<VALID, STATE> Entry<VALID, STATE> {
#[inline(always)] #[inline(always)]
/// If possible, return an iterator over the set of values transformed into a `&Uuid`. /// If possible, return an iterator over the set of values transformed into a `&Uuid`.
pub fn get_ava_as_refuuid(&self, attr: &str) -> Option<impl Iterator<Item = &Uuid>> { pub fn get_ava_as_refuuid(&self, attr: &str) -> Option<Box<dyn Iterator<Item = &Uuid> + '_>> {
// If any value is NOT a reference, it's filtered out. // If any value is NOT a reference, it's filtered out.
self.get_ava_set(attr).and_then(|vs| vs.as_ref_uuid_iter()) self.get_ava_set(attr).and_then(|vs| vs.as_ref_uuid_iter())
} }
@ -1694,6 +1707,10 @@ impl<VALID, STATE> Entry<VALID, STATE> {
self.attrs.get(attr).and_then(|vs| vs.to_uuid_single()) self.attrs.get(attr).and_then(|vs| vs.to_uuid_single())
} }
pub fn get_ava_single_refer(&self, attr: &str) -> Option<&Uuid> {
self.attrs.get(attr).and_then(|vs| vs.to_refer_single())
}
#[inline(always)] #[inline(always)]
/// Return a single protocol filter, if valid to transform this value. /// Return a single protocol filter, if valid to transform this value.
pub fn get_ava_single_protofilter(&self, attr: &str) -> Option<&ProtoFilter> { pub fn get_ava_single_protofilter(&self, attr: &str) -> Option<&ProtoFilter> {
@ -1860,9 +1877,11 @@ impl<VALID, STATE> Entry<VALID, STATE> {
// Get the schema attribute type out. // Get the schema attribute type out.
match schema.is_multivalue(k) { match schema.is_multivalue(k) {
Ok(r) => { Ok(r) => {
if !r { if !r || k == "systemmust" || k == "systemmay" {
// As this is single value, purge then present to maintain this // As this is single value, purge then present to maintain this
// invariant // invariant. The other situation we purge is within schema with
// the system types where we need to be able to express REMOVAL
// of attributes, thus we need the purge.
mods.push_mod(Modify::Purged(k.clone())); mods.push_mod(Modify::Purged(k.clone()));
} }
} }
@ -1907,21 +1926,30 @@ where
/// Remove an attribute-value pair from this entry. /// Remove an attribute-value pair from this entry.
fn remove_ava(&mut self, attr: &str, value: &PartialValue) { fn remove_ava(&mut self, attr: &str, value: &PartialValue) {
// It would be great to remove these extra allocations, but they let rm = if let Some(vs) = self.attrs.get_mut(attr) {
// really don't cost much :( vs.remove(value);
self.attrs.entry(AttrString::from(attr)).and_modify(|v| { vs.is_empty()
// Here we need to actually do a check/binary search ... } else {
v.remove(value); false
}); };
//
if rm {
self.attrs.remove(attr);
};
} }
// Need something that can remove by difference?
pub(crate) fn remove_avas(&mut self, attr: &str, values: &BTreeSet<PartialValue>) { pub(crate) fn remove_avas(&mut self, attr: &str, values: &BTreeSet<PartialValue>) {
if let Some(vs) = self.attrs.get_mut(attr) { let rm = if let Some(vs) = self.attrs.get_mut(attr) {
values.iter().for_each(|k| { values.iter().for_each(|k| {
vs.remove(k); vs.remove(k);
}) });
} vs.is_empty()
} else {
false
};
if rm {
self.attrs.remove(attr);
};
} }
/// Remove all values of this attribute from the entry. /// Remove all values of this attribute from the entry.
@ -2235,13 +2263,13 @@ mod tests {
e.apply_modlist(&present_single_mods); e.apply_modlist(&present_single_mods);
assert!(e.attribute_equality("attr", &PartialValue::new_iutf8("value"))); assert!(e.attribute_equality("attr", &PartialValue::new_iutf8("value")));
e.apply_modlist(&remove_mods); e.apply_modlist(&remove_mods);
assert!(e.attrs.get("attr").unwrap().is_empty()); assert!(e.attrs.get("attr").is_none());
let remove_empty_mods = remove_mods; let remove_empty_mods = remove_mods;
e.apply_modlist(&remove_empty_mods); e.apply_modlist(&remove_empty_mods);
assert!(e.attrs.get("attr").unwrap().is_empty()); assert!(e.attrs.get("attr").is_none());
} }
#[test] #[test]

View file

@ -151,4 +151,13 @@ impl Identity {
.attribute_equality("claim", &PartialValue::new_iutf8(claim)), .attribute_equality("claim", &PartialValue::new_iutf8(claim)),
} }
} }
pub fn is_memberof(&self, group: Uuid) -> bool {
match &self.origin {
IdentType::Internal => false,
IdentType::User(u) => u
.entry
.attribute_equality("memberof", &PartialValue::new_refer(group)),
}
}
} }

View file

@ -12,8 +12,10 @@ use fernet::Fernet;
use hashbrown::HashMap; use hashbrown::HashMap;
use kanidm_proto::v1::UserAuthToken; use kanidm_proto::v1::UserAuthToken;
use openssl::sha; use openssl::sha;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc; use std::sync::Arc;
use time::OffsetDateTime; use time::OffsetDateTime;
use tracing::trace;
use url::{Origin, Url}; use url::{Origin, Url};
use webauthn_rs::base64_data::Base64UrlSafeData; use webauthn_rs::base64_data::Base64UrlSafeData;
@ -76,6 +78,8 @@ struct ConsentToken {
pub code_challenge: Base64UrlSafeData, pub code_challenge: Base64UrlSafeData,
// Where the RS wants us to go back to. // Where the RS wants us to go back to.
pub redirect_uri: Url, pub redirect_uri: Url,
// The scopes being granted
pub scopes: Vec<String>,
} }
// consent token? // consent token?
@ -89,6 +93,8 @@ struct TokenExchangeCode {
pub code_challenge: Base64UrlSafeData, pub code_challenge: Base64UrlSafeData,
// The original redirect uri // The original redirect uri
pub redirect_uri: Url, pub redirect_uri: Url,
// The scopes being granted
pub scopes: Vec<String>,
} }
// consentPermitResponse // consentPermitResponse
@ -103,32 +109,43 @@ pub struct AuthorisePermitSuccess {
pub code: String, pub code: String,
} }
// The cache structure #[derive(Debug, Clone)]
pub enum Oauth2RSTokenFormat {
Uat,
}
#[derive(Clone)] #[derive(Clone)]
pub struct Oauth2RSBasic { pub struct Oauth2RS {
name: String, name: String,
displayname: String,
uuid: Uuid, uuid: Uuid,
origin: Origin, origin: Origin,
scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
implicit_scopes: Vec<String>,
// scope group map (hard)
// scope group map (soft)
// Client Auth Type (basic is all we support for now.
authz_secret: String, authz_secret: String,
// Our internal exchange encryption material for this rs.
token_fernet: Fernet, token_fernet: Fernet,
// What format we issue tokens as. By default prefer ANYTHING except jwt.
token_format: Oauth2RSTokenFormat,
} }
impl std::fmt::Debug for Oauth2RSBasic { impl std::fmt::Debug for Oauth2RS {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("Oauth2RSBasic") f.debug_struct("Oauth2RS")
.field("name", &self.name) .field("name", &self.name)
.field("displayname", &self.displayname)
.field("uuid", &self.uuid) .field("uuid", &self.uuid)
.field("origin", &self.origin) .field("origin", &self.origin)
.field("scope_maps", &self.scope_maps)
.field("implicit_scopes", &self.implicit_scopes)
.field("token_format", &self.token_format)
.finish() .finish()
} }
} }
#[derive(Debug, Clone)]
pub enum Oauth2RS {
Basic(Oauth2RSBasic),
}
#[derive(Clone)] #[derive(Clone)]
struct Oauth2RSInner { struct Oauth2RSInner {
fernet: Fernet, fernet: Fernet,
@ -186,44 +203,68 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
let rs_set: Result<HashMap<_, _>, _> = value let rs_set: Result<HashMap<_, _>, _> = value
.into_iter() .into_iter()
.map(|ent| { .map(|ent| {
let uuid = *ent.get_uuid();
admin_info!(?uuid, "Checking oauth2 configuration");
// From each entry, attempt to make an oauth2 configuration. // From each entry, attempt to make an oauth2 configuration.
if !ent.attribute_equality("class", &CLASS_OAUTH2) { if !ent.attribute_equality("class", &CLASS_OAUTH2) {
admin_error!("Missing class oauth2_resource_server");
// Check we have oauth2_resource_server class // Check we have oauth2_resource_server class
Err(OperationError::InvalidEntryState) Err(OperationError::InvalidEntryState)
} else if ent.attribute_equality("class", &CLASS_OAUTH2_BASIC) { } else if ent.attribute_equality("class", &CLASS_OAUTH2_BASIC) {
// If we have oauth2_resource_server_basic // If we have oauth2_resource_server_basic
// Now we know we can load the attrs. // Now we know we can load the attrs.
let uuid = *ent.get_uuid(); trace!("name");
let name = ent let name = ent
.get_ava_single_str("oauth2_rs_name") .get_ava_single_str("oauth2_rs_name")
.map(str::to_string) .map(str::to_string)
.ok_or(OperationError::InvalidValueState)?; .ok_or(OperationError::InvalidValueState)?;
trace!("displayname");
let displayname = ent
.get_ava_single_str("displayname")
.map(str::to_string)
.ok_or(OperationError::InvalidValueState)?;
trace!("origin");
let origin = ent let origin = ent
.get_ava_single_url("oauth2_rs_origin") .get_ava_single_url("oauth2_rs_origin")
.map(|url| url.origin()) .map(|url| url.origin())
.ok_or(OperationError::InvalidValueState)?; .ok_or(OperationError::InvalidValueState)?;
trace!("authz_secret");
let authz_secret = ent let authz_secret = ent
.get_ava_single_str("oauth2_rs_basic_secret") .get_ava_single_str("oauth2_rs_basic_secret")
.map(str::to_string) .map(str::to_string)
.ok_or(OperationError::InvalidValueState)?; .ok_or(OperationError::InvalidValueState)?;
trace!("token_key");
let token_fernet = ent let token_fernet = ent
.get_ava_single_secret("oauth2_rs_basic_token_key") .get_ava_single_secret("oauth2_rs_token_key")
.ok_or(OperationError::InvalidValueState) .ok_or(OperationError::InvalidValueState)
.and_then(|key| { .and_then(|key| {
Fernet::new(key).ok_or(OperationError::CryptographyError) Fernet::new(key).ok_or(OperationError::CryptographyError)
})?; })?;
// Currently unsure if this is how I want to handle this. trace!("scope_maps");
// let oauth2_rs_account_filter = ent.get_ava_single_protofilter("oauth2_rs_account_filter") let scope_maps = ent
.get_ava_as_oauthscopemaps("oauth2_rs_scope_map")
.cloned()
.unwrap_or_else(|| BTreeMap::new());
trace!("implicit_scopes");
let implicit_scopes = ent
.get_ava_as_oauthscopes("oauth2_rs_implicit_scopes")
.map(|iter| iter.map(str::to_string).collect())
.unwrap_or_else(|| Vec::new());
let client_id = name.clone(); let client_id = name.clone();
let rscfg = Oauth2RS::Basic(Oauth2RSBasic { let rscfg = Oauth2RS {
name, name,
displayname,
uuid, uuid,
origin, origin,
scope_maps,
implicit_scopes,
authz_secret, authz_secret,
token_fernet, token_fernet,
}); token_format: Oauth2RSTokenFormat::Uat,
};
Ok((client_id, rscfg)) Ok((client_id, rscfg))
} else { } else {
@ -275,28 +316,59 @@ impl Oauth2ResourceServersReadTransaction {
Oauth2Error::InvalidRequest Oauth2Error::InvalidRequest
})?; })?;
// scopes // redirect_uri must be part of the client_id origin.
if auth_req.redirect_uri.origin() != o2rs.origin {
admin_warn!(
origin = ?o2rs.origin,
"Invalid oauth2 redirect_uri (must be related to origin of)"
);
return Err(Oauth2Error::InvalidRequest);
}
// user authorisation filter // scopes - you need to have every requested scope or this req is denied.
let req_scopes: BTreeSet<_> = auth_req.scope.split_ascii_whitespace().collect();
if req_scopes.is_empty() {
admin_error!("Invalid oauth2 request - must contain at least one requested scope");
return Err(Oauth2Error::InvalidRequest);
}
let uat_scopes: BTreeSet<_> = o2rs
.implicit_scopes
.iter()
.map(|s| s.as_str())
.chain(
o2rs.scope_maps
.iter()
.filter_map(|(u, m)| {
if ident.is_memberof(*u) {
Some(m.iter().map(|s| s.as_str()))
} else {
None
}
})
.flatten(),
)
.collect();
// Needs to use s.to_string due to &&str which can't use the str::to_string
let avail_scopes: Vec<String> = req_scopes
.intersection(&uat_scopes)
.map(|s| s.to_string())
.collect();
if avail_scopes.len() != req_scopes.len() {
admin_warn!(
%ident,
%auth_req.scope,
"Identity does not have access to the requested scopes"
);
return Err(Oauth2Error::AccessDenied);
}
// Subseqent we then return an encrypted session handle which allows // Subseqent we then return an encrypted session handle which allows
// the user to indicate their consent to this authorisation. // the user to indicate their consent to this authorisation.
// //
// This session handle is what we use in "permit" to generate the redirect. // This session handle is what we use in "permit" to generate the redirect.
match o2rs {
Oauth2RS::Basic(rsbasic) => {
// redirect_uri must be part of the client_id origin.
if auth_req.redirect_uri.origin() != rsbasic.origin {
admin_warn!(
origin = ?rsbasic.origin,
"Invalid oauth2 redirect_uri (must be related to origin of)"
);
return Err(Oauth2Error::InvalidRequest);
}
}
};
let consent_req = ConsentToken { let consent_req = ConsentToken {
client_id: auth_req.client_id.clone(), client_id: auth_req.client_id.clone(),
ident_id: ident.get_event_origin_id(), ident_id: ident.get_event_origin_id(),
@ -304,6 +376,7 @@ impl Oauth2ResourceServersReadTransaction {
state: auth_req.state.clone(), state: auth_req.state.clone(),
code_challenge: auth_req.code_challenge.clone(), code_challenge: auth_req.code_challenge.clone(),
redirect_uri: auth_req.redirect_uri.clone(), redirect_uri: auth_req.redirect_uri.clone(),
scopes: avail_scopes.clone(),
}; };
let consent_data = serde_json::to_vec(&consent_req).map_err(|e| { let consent_data = serde_json::to_vec(&consent_req).map_err(|e| {
@ -317,8 +390,8 @@ impl Oauth2ResourceServersReadTransaction {
.encrypt_at_time(&consent_data, ct.as_secs()); .encrypt_at_time(&consent_data, ct.as_secs());
Ok(ConsentRequest { Ok(ConsentRequest {
client_name: auth_req.client_id.clone(), client_name: o2rs.displayname.clone(),
scopes: Vec::new(), scopes: avail_scopes,
consent_token, consent_token,
}) })
} }
@ -359,13 +432,14 @@ impl Oauth2ResourceServersReadTransaction {
} }
// Get the resource server config based on this client_id. // Get the resource server config based on this client_id.
let o2rs_fernet = match self.inner.rs_set.get(&consent_req.client_id) { let o2rs = self
Some(Oauth2RS::Basic(rsbasic)) => &rsbasic.token_fernet, .inner
None => { .rs_set
.get(&consent_req.client_id)
.ok_or_else(|| {
admin_error!("Invalid consent request oauth2 client_id"); admin_error!("Invalid consent request oauth2 client_id");
return Err(OperationError::InvalidRequestState); OperationError::InvalidRequestState
} })?;
};
// Extract the state, code challenge, redirect_uri // Extract the state, code challenge, redirect_uri
@ -373,6 +447,7 @@ impl Oauth2ResourceServersReadTransaction {
uat: uat.clone(), uat: uat.clone(),
code_challenge: consent_req.code_challenge, code_challenge: consent_req.code_challenge,
redirect_uri: consent_req.redirect_uri.clone(), redirect_uri: consent_req.redirect_uri.clone(),
scopes: consent_req.scopes,
}; };
// Encrypt the exchange token with the fernet key of the client resource server // Encrypt the exchange token with the fernet key of the client resource server
@ -381,7 +456,7 @@ impl Oauth2ResourceServersReadTransaction {
OperationError::SerdeJsonError OperationError::SerdeJsonError
})?; })?;
let code = o2rs_fernet.encrypt_at_time(&code_data, ct.as_secs()); let code = o2rs.token_fernet.encrypt_at_time(&code_data, ct.as_secs());
Ok(AuthorisePermitSuccess { Ok(AuthorisePermitSuccess {
redirect_uri: consent_req.redirect_uri, redirect_uri: consent_req.redirect_uri,
@ -434,21 +509,17 @@ impl Oauth2ResourceServersReadTransaction {
})?; })?;
// check the secret. // check the secret.
let o2rs_fernet = match o2rs { if o2rs.authz_secret != secret {
Oauth2RS::Basic(rsbasic) => { security_info!("Invalid oauth2 client_id secret");
if rsbasic.authz_secret != secret { return Err(Oauth2Error::AuthenticationRequired);
security_info!("Invalid oauth2 client_id secret"); }
return Err(Oauth2Error::AuthenticationRequired); // We are authenticated! Yay! Now we can actually check things ...
}
// We are authenticated! Yay! Now we can actually check things ...
&rsbasic.token_fernet
}
};
// Check the token_req is within the valid time, and correctly signed for // Check the token_req is within the valid time, and correctly signed for
// this client. // this client.
let code_xchg: TokenExchangeCode = o2rs_fernet let code_xchg: TokenExchangeCode = o2rs
.token_fernet
.decrypt_at_time(&token_req.code, Some(60), ct.as_secs()) .decrypt_at_time(&token_req.code, Some(60), ct.as_secs())
.map_err(|_| { .map_err(|_| {
admin_error!("Failed to decrypt token exchange request"); admin_error!("Failed to decrypt token exchange request");
@ -491,19 +562,30 @@ impl Oauth2ResourceServersReadTransaction {
return Err(Oauth2Error::AccessDenied); return Err(Oauth2Error::AccessDenied);
}; };
let access_token = serde_json::to_vec(&code_xchg.uat) let scope = if code_xchg.scopes.is_empty() {
.map_err(|e| { None
admin_error!(err = ?e, "Unable to encode uat data"); } else {
Oauth2Error::ServerError(OperationError::SerdeJsonError) Some(code_xchg.scopes.join(" "))
}) };
.map(|data| o2rs_fernet.encrypt_at_time(&data, ct.as_secs()))?;
// If we are type == Uat, then we re-use the same encryption material here.
let access_token_data = serde_json::to_vec(&code_xchg.uat).map_err(|e| {
admin_error!(err = ?e, "Unable to encode uat data");
Oauth2Error::ServerError(OperationError::SerdeJsonError)
})?;
let access_token = match o2rs.token_format {
Oauth2RSTokenFormat::Uat => o2rs
.token_fernet
.encrypt_at_time(&access_token_data, ct.as_secs()),
};
Ok(AccessTokenResponse { Ok(AccessTokenResponse {
access_token, access_token,
token_type: "bearer".to_string(), token_type: "bearer".to_string(),
expires_in, expires_in,
refresh_token: None, refresh_token: None,
scope: None, scope,
}) })
} }
} }
@ -552,7 +634,7 @@ mod tests {
code_challenge: Base64UrlSafeData($code_challenge), code_challenge: Base64UrlSafeData($code_challenge),
code_challenge_method: CodeChallengeMethod::S256, code_challenge_method: CodeChallengeMethod::S256,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(), redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: "".to_string(), scope: "test".to_string(),
}; };
$idms_prox_read $idms_prox_read
@ -576,9 +658,16 @@ mod tests {
("class", Value::new_class("oauth2_resource_server_basic")), ("class", Value::new_class("oauth2_resource_server_basic")),
("uuid", Value::new_uuid(uuid)), ("uuid", Value::new_uuid(uuid)),
("oauth2_rs_name", Value::new_iname("test_resource_server")), ("oauth2_rs_name", Value::new_iname("test_resource_server")),
("displayname", Value::new_utf8s("test_resource_server")),
( (
"oauth2_rs_origin", "oauth2_rs_origin",
Value::new_url_s("https://demo.example.com").unwrap() Value::new_url_s("https://demo.example.com").unwrap()
),
("oauth2_rs_implicit_scopes", Value::new_oauthscope("test")),
// System admins
(
"oauth2_rs_scope_map",
Value::new_oauthscopemap(*UUID_SYSTEM_ADMINS, btreeset!["read".to_string()])
) )
); );
let ce = CreateEvent::new_internal(vec![e]); let ce = CreateEvent::new_internal(vec![e]);
@ -610,6 +699,24 @@ mod tests {
(secret, uat, ident) (secret, uat, ident)
} }
fn setup_anon(idms: &IdmServer, ct: Duration) -> (UserAuthToken, Identity) {
let mut idms_prox_write = idms.proxy_write(ct);
let account = idms_prox_write
.target_to_account(&UUID_IDM_ADMIN)
.expect("account must exist");
let session_id = uuid::Uuid::new_v4();
let uat = account
.to_userauthtoken(session_id, ct, AuthType::Anonymous)
.expect("Unable to create uat");
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct)
.expect("Unable to process uat");
idms_prox_write.commit().expect("failed to commit");
(uat, ident)
}
#[test] #[test]
fn test_idm_oauth2_basic_function() { fn test_idm_oauth2_basic_function() {
run_idm_test!(|_qs: &QueryServer, run_idm_test!(|_qs: &QueryServer,
@ -666,6 +773,10 @@ mod tests {
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident) = setup_oauth2_resource_server(idms, ct); let (_secret, uat, ident) = setup_oauth2_resource_server(idms, ct);
let (anon_uat, anon_ident) = setup_anon(idms, ct);
// Need a uat from a user not in the group. Probs anonymous.
let idms_prox_read = idms.proxy_read(); let idms_prox_read = idms.proxy_read();
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble"); let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
@ -678,7 +789,7 @@ mod tests {
code_challenge: Base64UrlSafeData(code_challenge.clone()), code_challenge: Base64UrlSafeData(code_challenge.clone()),
code_challenge_method: CodeChallengeMethod::S256, code_challenge_method: CodeChallengeMethod::S256,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(), redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: "".to_string(), scope: "test".to_string(),
}; };
assert!( assert!(
@ -696,7 +807,7 @@ mod tests {
code_challenge: Base64UrlSafeData(code_challenge.clone()), code_challenge: Base64UrlSafeData(code_challenge.clone()),
code_challenge_method: CodeChallengeMethod::S256, code_challenge_method: CodeChallengeMethod::S256,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(), redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: "".to_string(), scope: "test".to_string(),
}; };
assert!( assert!(
@ -711,10 +822,10 @@ mod tests {
response_type: "code".to_string(), response_type: "code".to_string(),
client_id: "test_resource_server".to_string(), client_id: "test_resource_server".to_string(),
state: Base64UrlSafeData(vec![1, 2, 3]), state: Base64UrlSafeData(vec![1, 2, 3]),
code_challenge: Base64UrlSafeData(code_challenge), code_challenge: Base64UrlSafeData(code_challenge.clone()),
code_challenge_method: CodeChallengeMethod::S256, code_challenge_method: CodeChallengeMethod::S256,
redirect_uri: Url::parse("https://totes.not.sus.org/oauth2/result").unwrap(), redirect_uri: Url::parse("https://totes.not.sus.org/oauth2/result").unwrap(),
scope: "".to_string(), scope: "test".to_string(),
}; };
assert!( assert!(
@ -723,6 +834,43 @@ mod tests {
.unwrap_err() .unwrap_err()
== Oauth2Error::InvalidRequest == Oauth2Error::InvalidRequest
); );
// Requested scope is not available
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: Base64UrlSafeData(vec![1, 2, 3]),
code_challenge: Base64UrlSafeData(code_challenge.clone()),
code_challenge_method: CodeChallengeMethod::S256,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: "invalid_scope read".to_string(),
};
assert!(
idms_prox_read
.check_oauth2_authorisation(&ident, &uat, &auth_req, ct)
.unwrap_err()
== Oauth2Error::AccessDenied
);
// Not a member of the group.
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: Base64UrlSafeData(vec![1, 2, 3]),
code_challenge: Base64UrlSafeData(code_challenge),
code_challenge_method: CodeChallengeMethod::S256,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: "read test".to_string(),
};
assert!(
idms_prox_read
.check_oauth2_authorisation(&anon_ident, &anon_uat, &auth_req, ct)
.unwrap_err()
== Oauth2Error::AccessDenied
);
}) })
} }

View file

@ -1155,7 +1155,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
} }
} }
// TODO: tracing
pub(crate) fn target_to_account(&mut self, target: &Uuid) -> Result<Account, OperationError> { pub(crate) fn target_to_account(&mut self, target: &Uuid) -> Result<Account, OperationError> {
// Get the account // Get the account
let account = self let account = self
@ -1172,24 +1171,19 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
// Deny the change if the account is anonymous! // Deny the change if the account is anonymous!
if account.is_anonymous() { if account.is_anonymous() {
admin_warn!("Unable to convert anonymous to account during write txn");
Err(OperationError::SystemProtectedObject) Err(OperationError::SystemProtectedObject)
} else { } else {
Ok(account) Ok(account)
} }
} }
// TODO: tracing
pub fn set_account_password( pub fn set_account_password(
&mut self, &mut self,
pce: &PasswordChangeEvent, pce: &PasswordChangeEvent,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
let account = self.target_to_account(&pce.target)?; let account = self.target_to_account(&pce.target)?;
// Deny the change if the account is anonymous!
if account.is_anonymous() {
return Err(OperationError::SystemProtectedObject);
}
// Get the modifications we *want* to perform. // Get the modifications we *want* to perform.
let modlist = account let modlist = account
.gen_password_mod(pce.cleartext.as_str(), self.crypto_policy) .gen_password_mod(pce.cleartext.as_str(), self.crypto_policy)
@ -1257,7 +1251,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
Ok(()) Ok(())
} }
// TODO: tracing
pub fn set_unix_account_password( pub fn set_unix_account_password(
&mut self, &mut self,
pce: &UnixPasswordChangeEvent, pce: &UnixPasswordChangeEvent,

View file

@ -100,6 +100,10 @@ impl ModifyList<ModifyInvalid> {
Self::new_list(vec![Modify::Present(AttrString::from(attr), v)]) Self::new_list(vec![Modify::Present(AttrString::from(attr), v)])
} }
pub fn new_remove(attr: &str, pv: PartialValue) -> Self {
Self::new_list(vec![Modify::Removed(AttrString::from(attr), pv)])
}
pub fn new_purge(attr: &str) -> Self { pub fn new_purge(attr: &str) -> Self {
Self::new_list(vec![m_purge(attr)]) Self::new_list(vec![m_purge(attr)])
} }

View file

@ -20,11 +20,11 @@ macro_rules! oauth2_transform {
let v = Value::new_utf8(password_from_random()); let v = Value::new_utf8(password_from_random());
$e.add_ava("oauth2_rs_basic_secret", v); $e.add_ava("oauth2_rs_basic_secret", v);
} }
if !$e.attribute_pres("oauth2_rs_basic_token_key") { if !$e.attribute_pres("oauth2_rs_token_key") {
security_info!("regenerating oauth2 token key"); security_info!("regenerating oauth2 token key");
let k = fernet::Fernet::generate_key(); let k = fernet::Fernet::generate_key();
let v = Value::new_secret_str(&k); let v = Value::new_secret_str(&k);
$e.add_ava("oauth2_rs_basic_token_key", v); $e.add_ava("oauth2_rs_token_key", v);
} }
} }
Ok(()) Ok(())
@ -68,11 +68,13 @@ mod tests {
("class", Value::new_class("oauth2_resource_server")), ("class", Value::new_class("oauth2_resource_server")),
("class", Value::new_class("oauth2_resource_server_basic")), ("class", Value::new_class("oauth2_resource_server_basic")),
("uuid", Value::new_uuid(uuid)), ("uuid", Value::new_uuid(uuid)),
("displayname", Value::new_utf8s("test_resource_server")),
("oauth2_rs_name", Value::new_iname("test_resource_server")), ("oauth2_rs_name", Value::new_iname("test_resource_server")),
( (
"oauth2_rs_origin", "oauth2_rs_origin",
Value::new_url_s("https://demo.example.com").unwrap() Value::new_url_s("https://demo.example.com").unwrap()
) ),
("oauth2_rs_implicit_scopes", Value::new_oauthscope("read"))
); );
let create = vec![e]; let create = vec![e];
@ -87,7 +89,7 @@ mod tests {
.internal_search_uuid(&uuid) .internal_search_uuid(&uuid)
.expect("failed to get oauth2 config"); .expect("failed to get oauth2 config");
assert!(e.attribute_pres("oauth2_rs_basic_secret")); assert!(e.attribute_pres("oauth2_rs_basic_secret"));
assert!(e.attribute_pres("oauth2_rs_basic_token_key")); assert!(e.attribute_pres("oauth2_rs_token_key"));
} }
); );
} }
@ -102,12 +104,14 @@ mod tests {
("class", Value::new_class("oauth2_resource_server_basic")), ("class", Value::new_class("oauth2_resource_server_basic")),
("uuid", Value::new_uuid(uuid)), ("uuid", Value::new_uuid(uuid)),
("oauth2_rs_name", Value::new_iname("test_resource_server")), ("oauth2_rs_name", Value::new_iname("test_resource_server")),
("displayname", Value::new_utf8s("test_resource_server")),
( (
"oauth2_rs_origin", "oauth2_rs_origin",
Value::new_url_s("https://demo.example.com").unwrap() Value::new_url_s("https://demo.example.com").unwrap()
), ),
("oauth2_rs_implicit_scopes", Value::new_oauthscope("read")),
("oauth2_rs_basic_secret", Value::new_utf8s("12345")), ("oauth2_rs_basic_secret", Value::new_utf8s("12345")),
("oauth2_rs_basic_token_key", Value::new_secret_str("12345")) ("oauth2_rs_token_key", Value::new_secret_str("12345"))
); );
let preload = vec![e]; let preload = vec![e];
@ -118,7 +122,7 @@ mod tests {
filter!(f_eq("uuid", PartialValue::new_uuid(uuid))), filter!(f_eq("uuid", PartialValue::new_uuid(uuid))),
ModifyList::new_list(vec![ ModifyList::new_list(vec![
Modify::Purged(AttrString::from("oauth2_rs_basic_secret"),), Modify::Purged(AttrString::from("oauth2_rs_basic_secret"),),
Modify::Purged(AttrString::from("oauth2_rs_basic_token_key"),) Modify::Purged(AttrString::from("oauth2_rs_token_key"),)
]), ]),
None, None,
|qs: &QueryServerWriteTransaction| { |qs: &QueryServerWriteTransaction| {
@ -126,10 +130,10 @@ mod tests {
.internal_search_uuid(&uuid) .internal_search_uuid(&uuid)
.expect("failed to get oauth2 config"); .expect("failed to get oauth2 config");
assert!(e.attribute_pres("oauth2_rs_basic_secret")); assert!(e.attribute_pres("oauth2_rs_basic_secret"));
assert!(e.attribute_pres("oauth2_rs_basic_token_key")); assert!(e.attribute_pres("oauth2_rs_token_key"));
// Check the values are different. // Check the values are different.
assert!(e.get_ava_single_str("oauth2_rs_basic_secret") != Some("12345")); assert!(e.get_ava_single_str("oauth2_rs_basic_secret") != Some("12345"));
assert!(e.get_ava_single_secret("oauth2_rs_basic_token_key") != Some("12345")); assert!(e.get_ava_single_secret("oauth2_rs_token_key") != Some("12345"));
} }
); );
} }

View file

@ -21,6 +21,7 @@ use crate::modify::Modify;
use crate::schema::SchemaTransaction; use crate::schema::SchemaTransaction;
use kanidm_proto::v1::{ConsistencyError, PluginError}; use kanidm_proto::v1::{ConsistencyError, PluginError};
use std::sync::Arc; use std::sync::Arc;
use tracing::trace;
// NOTE: This *must* be after base.rs!!! // NOTE: This *must* be after base.rs!!!
@ -33,7 +34,7 @@ impl ReferentialIntegrity {
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
if inner.is_empty() { if inner.is_empty() {
// There is nothing to check! Move on. // There is nothing to check! Move on.
admin_info!("no reference types modified, skipping check"); trace!("no reference types modified, skipping check");
return Ok(()); return Ok(());
} }
@ -109,7 +110,7 @@ impl Plugin for ReferentialIntegrity {
}); });
Ok(()) Ok(())
} else { } else {
admin_error!("reference value could not convert to reference uuid."); admin_error!(?vs, "reference value could not convert to reference uuid.");
admin_error!("If you are sure the name/uuid/spn exist, and that this is in error, you should run a verify task."); admin_error!("If you are sure the name/uuid/spn exist, and that this is in error, you should run a verify task.");
Err(OperationError::InvalidAttribute( Err(OperationError::InvalidAttribute(
"uuid could not become reference value".to_string(), "uuid could not become reference value".to_string(),
@ -144,7 +145,7 @@ impl Plugin for ReferentialIntegrity {
v.to_ref_uuid() v.to_ref_uuid()
.map(|uuid| PartialValue::new_uuid(*uuid)) .map(|uuid| PartialValue::new_uuid(*uuid))
.ok_or_else(|| { .ok_or_else(|| {
admin_error!("reference value could not convert to reference uuid."); admin_error!(?v, "reference value could not convert to reference uuid.");
admin_error!("If you are sure the name/uuid/spn exist, and that this is in error, you should run a verify task."); admin_error!("If you are sure the name/uuid/spn exist, and that this is in error, you should run a verify task.");
OperationError::InvalidAttribute( OperationError::InvalidAttribute(
"uuid could not become reference value".to_string(), "uuid could not become reference value".to_string(),
@ -698,4 +699,61 @@ mod tests {
|_qs: &QueryServerWriteTransaction| {} |_qs: &QueryServerWriteTransaction| {}
); );
} }
#[test]
fn test_delete_remove_reference_oauth2() {
// Oauth2 types are also capable of uuid referencing to groups for their
// scope maps, so we need to check that when the group is deleted, that the
// scope map is also appropriately affected.
let ea: Entry<EntryInit, EntryNew> = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("oauth2_resource_server")),
("class", Value::new_class("oauth2_resource_server_basic")),
("oauth2_rs_name", Value::new_iname("test_resource_server")),
("displayname", Value::new_utf8s("test_resource_server")),
(
"oauth2_rs_origin",
Value::new_url_s("https://demo.example.com").unwrap()
),
("oauth2_rs_implicit_scopes", Value::new_oauthscope("test")),
(
"oauth2_rs_scope_map",
Value::new_oauthscopemap(
Uuid::parse_str("cc8e95b4-c24f-4d68-ba54-8bed76f63930").expect("uuid"),
btreeset!["read".to_string()]
)
)
);
let eb: Entry<EntryInit, EntryNew> = entry_init!(
("class", Value::new_class("group")),
("name", Value::new_iname("testgroup")),
(
"uuid",
Value::new_uuids("cc8e95b4-c24f-4d68-ba54-8bed76f63930").expect("uuid")
),
("description", Value::new_utf8s("testgroup"))
);
let preload = vec![ea, eb];
run_delete_test!(
Ok(()),
preload,
filter!(f_eq("name", PartialValue::new_iname("testgroup"))),
None,
|qs: &QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
"oauth2_rs_name",
PartialValue::new_iname("test_resource_server")
)))
.expect("Internal search failure");
let ue = cands.first().expect("No entry");
assert!(ue
.get_ava_as_oauthscopemaps("oauth2_rs_scope_map")
.is_none())
}
);
}
} }

View file

@ -196,10 +196,13 @@ impl SchemaAttribute {
SyntaxType::DateTime => v.is_datetime(), SyntaxType::DateTime => v.is_datetime(),
SyntaxType::EmailAddress => v.is_email_address(), SyntaxType::EmailAddress => v.is_email_address(),
SyntaxType::Url => v.is_url(), SyntaxType::Url => v.is_url(),
SyntaxType::OauthScope => v.is_oauthscope(),
SyntaxType::OauthScopeMap => v.is_oauthscopemap() || v.is_refer(),
}; };
if r { if r {
Ok(()) Ok(())
} else { } else {
trace!(?a, ?self, ?v, "validate_pv InvalidAttributeSyntax");
Err(SchemaError::InvalidAttributeSyntax(a.to_string())) Err(SchemaError::InvalidAttributeSyntax(a.to_string()))
} }
} }
@ -213,6 +216,12 @@ impl SchemaAttribute {
let pv: &PartialValue = v.borrow(); let pv: &PartialValue = v.borrow();
self.validate_partialvalue(a, pv) self.validate_partialvalue(a, pv)
} else { } else {
trace!(
?a,
?self,
?v,
"value validation failure - InvalidAttributeSyntax"
);
Err(SchemaError::InvalidAttributeSyntax(a.to_string())) Err(SchemaError::InvalidAttributeSyntax(a.to_string()))
} }
} }
@ -222,6 +231,7 @@ impl SchemaAttribute {
// Check multivalue // Check multivalue
if !self.multivalue && ava.len() > 1 { if !self.multivalue && ava.len() > 1 {
// lrequest_error!("Ava len > 1 on single value attribute!"); // lrequest_error!("Ava len > 1 on single value attribute!");
admin_error!("Ava len > 1 on single value attribute!");
return Err(SchemaError::InvalidAttributeSyntax(a.to_string())); return Err(SchemaError::InvalidAttributeSyntax(a.to_string()));
}; };
// If syntax, check the type is correct // If syntax, check the type is correct
@ -245,11 +255,13 @@ impl SchemaAttribute {
SyntaxType::DateTime => ava.is_datetime(), SyntaxType::DateTime => ava.is_datetime(),
SyntaxType::EmailAddress => ava.is_email_address(), SyntaxType::EmailAddress => ava.is_email_address(),
SyntaxType::Url => ava.is_url(), SyntaxType::Url => ava.is_url(),
SyntaxType::OauthScope => ava.is_oauthscope(),
SyntaxType::OauthScopeMap => ava.is_oauthscopemap(),
}; };
if valid { if valid {
Ok(()) Ok(())
} else { } else {
trace!(?a, "InvalidAttributeSyntax"); admin_error!(?a, "validate_ava - InvalidAttributeSyntax");
Err(SchemaError::InvalidAttributeSyntax(a.to_string())) Err(SchemaError::InvalidAttributeSyntax(a.to_string()))
} }
} }
@ -466,7 +478,7 @@ impl<'a> SchemaWriteTransaction<'a> {
// No, they'll over-write each other ... but we do need name uniqueness. // No, they'll over-write each other ... but we do need name uniqueness.
attributetypes.into_iter().for_each(|a| { attributetypes.into_iter().for_each(|a| {
// Update the unique and ref caches. // Update the unique and ref caches.
if a.syntax == SyntaxType::REFERENCE_UUID { if a.syntax == SyntaxType::REFERENCE_UUID || a.syntax == SyntaxType::OauthScopeMap {
self.ref_cache.insert(a.name.clone(), a.clone()); self.ref_cache.insert(a.name.clone(), a.clone());
} }
if a.unique { if a.unique {
@ -1837,6 +1849,7 @@ mod tests {
#[test] #[test]
fn test_schema_entries() { fn test_schema_entries() {
let _ = crate::tracing_tree::test_init();
// Given an entry, assert it's schema is valid // Given an entry, assert it's schema is valid
// We do // We do
let schema_outer = Schema::new().expect("failed to create schema"); let schema_outer = Schema::new().expect("failed to create schema");

View file

@ -513,6 +513,8 @@ pub trait QueryServerTransaction<'a> {
SyntaxType::EmailAddress => Ok(Value::new_email_address_s(value)), SyntaxType::EmailAddress => Ok(Value::new_email_address_s(value)),
SyntaxType::Url => Value::new_url_s(value) SyntaxType::Url => Value::new_url_s(value)
.ok_or_else(|| OperationError::InvalidAttribute("Invalid Url (whatwg/url) syntax".to_string())), .ok_or_else(|| OperationError::InvalidAttribute("Invalid Url (whatwg/url) syntax".to_string())),
SyntaxType::OauthScope => Ok(Value::new_oauthscope(value)),
SyntaxType::OauthScopeMap => Err(OperationError::InvalidAttribute("Oauth Scope Maps can not be supplied through modification - please use the IDM api".to_string())),
} }
} }
None => { None => {
@ -589,6 +591,24 @@ pub trait QueryServerTransaction<'a> {
) )
}) })
} }
SyntaxType::OauthScopeMap => {
// See comments above.
PartialValue::new_oauthscopemap_s(value)
.or_else(|| {
let un = self
.name_to_uuid(value)
.unwrap_or_else(|_| *UUID_DOES_NOT_EXIST);
Some(PartialValue::new_oauthscopemap(un))
})
// I think this is unreachable due to how the .or_else works.
// See above case for how to avoid having unreachable code
.ok_or_else(|| {
OperationError::InvalidAttribute(
"Invalid Reference syntax".to_string(),
)
})
}
SyntaxType::JSON_FILTER => { SyntaxType::JSON_FILTER => {
PartialValue::new_json_filter_s(value).ok_or_else(|| { PartialValue::new_json_filter_s(value).ok_or_else(|| {
OperationError::InvalidAttribute("Invalid Filter syntax".to_string()) OperationError::InvalidAttribute("Invalid Filter syntax".to_string())
@ -620,6 +640,7 @@ pub trait QueryServerTransaction<'a> {
"Invalid Url (whatwg/url) syntax".to_string(), "Invalid Url (whatwg/url) syntax".to_string(),
) )
}), }),
SyntaxType::OauthScope => Ok(PartialValue::new_oauthscope(value)),
} }
} }
None => { None => {
@ -644,6 +665,19 @@ pub trait QueryServerTransaction<'a> {
}) })
.collect(); .collect();
v v
} else if let Some(r_map) = value.as_oauthscopemap() {
let v: Result<Vec<_>, _> = r_map
.iter()
.map(|(u, m)| {
let nv = self.uuid_to_spn(u)?;
let u = match nv {
Some(v) => v.to_proto_string_clone(),
None => ValueSet::uuid_to_proto_string(u),
};
Ok(format!("{}: {:?}", u, m))
})
.collect();
v
} else { } else {
let v: Vec<_> = value.to_proto_string_clone_iter().collect(); let v: Vec<_> = value.to_proto_string_clone_iter().collect();
Ok(v) Ok(v)
@ -2049,7 +2083,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
// //
// NOTE: gen modlist IS schema aware and will handle multivalue // NOTE: gen modlist IS schema aware and will handle multivalue
// correctly! // correctly!
trace!("internal_migrate_or_create operating on {:?}", e.get_uuid()); admin_info!("internal_migrate_or_create operating on {:?}", e.get_uuid());
let filt = match e.filter_from_attrs(&[AttrString::from("uuid")]) { let filt = match e.filter_from_attrs(&[AttrString::from("uuid")]) {
Some(f) => f, Some(f) => f,
@ -2189,9 +2223,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_SCHEMA_ATTR_ACCOUNT_VALID_FROM, JSON_SCHEMA_ATTR_ACCOUNT_VALID_FROM,
JSON_SCHEMA_ATTR_OAUTH2_RS_NAME, JSON_SCHEMA_ATTR_OAUTH2_RS_NAME,
JSON_SCHEMA_ATTR_OAUTH2_RS_ORIGIN, JSON_SCHEMA_ATTR_OAUTH2_RS_ORIGIN,
JSON_SCHEMA_ATTR_OAUTH2_RS_ACCOUNT_FILTER, JSON_SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP,
JSON_SCHEMA_ATTR_OAUTH2_RS_IMPLICIT_SCOPES,
JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET, JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET,
JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY, JSON_SCHEMA_ATTR_OAUTH2_RS_TOKEN_KEY,
JSON_SCHEMA_CLASS_PERSON, JSON_SCHEMA_CLASS_PERSON,
JSON_SCHEMA_CLASS_GROUP, JSON_SCHEMA_CLASS_GROUP,
JSON_SCHEMA_CLASS_ACCOUNT, JSON_SCHEMA_CLASS_ACCOUNT,

View file

@ -4,13 +4,15 @@
//! these into a form for the backend that can be persistent into the [`Backend`](crate::be::Backend). //! these into a form for the backend that can be persistent into the [`Backend`](crate::be::Backend).
use crate::be::dbvalue::{ use crate::be::dbvalue::{
DbCidV1, DbValueCredV1, DbValueEmailAddressV1, DbValueTaggedStringV1, DbValueV1, DbCidV1, DbValueCredV1, DbValueEmailAddressV1, DbValueOauthScopeMapV1, DbValueTaggedStringV1,
DbValueV1,
}; };
use crate::credential::Credential; use crate::credential::Credential;
use crate::repl::cid::Cid; use crate::repl::cid::Cid;
use kanidm_proto::v1::Filter as ProtoFilter; use kanidm_proto::v1::Filter as ProtoFilter;
use std::borrow::Borrow; use std::borrow::Borrow;
use std::collections::BTreeSet;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fmt; use std::fmt;
use std::str::FromStr; use std::str::FromStr;
@ -43,6 +45,11 @@ lazy_static! {
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
Regex::new("^[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}$").expect("Invalid Nsunique regex found") Regex::new("^[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}$").expect("Invalid Nsunique regex found")
}; };
static ref OAUTHSCOPE_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new("^[0-9a-zA-Z_]+$").expect("Invalid oauthscope regex found")
// Must not contain whitespace.
};
} }
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
@ -136,6 +143,8 @@ pub enum SyntaxType {
DateTime, DateTime,
EmailAddress, EmailAddress,
Url, Url,
OauthScope,
OauthScopeMap,
} }
impl TryFrom<&str> for SyntaxType { impl TryFrom<&str> for SyntaxType {
@ -164,6 +173,8 @@ impl TryFrom<&str> for SyntaxType {
"DATETIME" => Ok(SyntaxType::DateTime), "DATETIME" => Ok(SyntaxType::DateTime),
"EMAIL_ADDRESS" => Ok(SyntaxType::EmailAddress), "EMAIL_ADDRESS" => Ok(SyntaxType::EmailAddress),
"URL" => Ok(SyntaxType::Url), "URL" => Ok(SyntaxType::Url),
"OAUTH_SCOPE" => Ok(SyntaxType::OauthScope),
"OAUTH_SCOPE_MAP" => Ok(SyntaxType::OauthScopeMap),
_ => Err(()), _ => Err(()),
} }
} }
@ -193,6 +204,8 @@ impl TryFrom<usize> for SyntaxType {
16 => Ok(SyntaxType::DateTime), 16 => Ok(SyntaxType::DateTime),
17 => Ok(SyntaxType::EmailAddress), 17 => Ok(SyntaxType::EmailAddress),
18 => Ok(SyntaxType::Url), 18 => Ok(SyntaxType::Url),
19 => Ok(SyntaxType::OauthScope),
20 => Ok(SyntaxType::OauthScopeMap),
_ => Err(()), _ => Err(()),
} }
} }
@ -220,6 +233,8 @@ impl SyntaxType {
SyntaxType::DateTime => 16, SyntaxType::DateTime => 16,
SyntaxType::EmailAddress => 17, SyntaxType::EmailAddress => 17,
SyntaxType::Url => 18, SyntaxType::Url => 18,
SyntaxType::OauthScope => 19,
SyntaxType::OauthScopeMap => 20,
} }
} }
} }
@ -246,6 +261,8 @@ impl fmt::Display for SyntaxType {
SyntaxType::DateTime => "DATETIME", SyntaxType::DateTime => "DATETIME",
SyntaxType::EmailAddress => "EMAIL_ADDRESS", SyntaxType::EmailAddress => "EMAIL_ADDRESS",
SyntaxType::Url => "URL", SyntaxType::Url => "URL",
SyntaxType::OauthScope => "OAUTH_SCOPE",
SyntaxType::OauthScopeMap => "OAUTH_SCOPE_MAP",
}) })
} }
} }
@ -255,6 +272,7 @@ pub enum DataValue {
Cred(Credential), Cred(Credential),
SshKey(String), SshKey(String),
SecretValue(String), SecretValue(String),
OauthScopeMap(BTreeSet<String>),
} }
impl std::fmt::Debug for DataValue { impl std::fmt::Debug for DataValue {
@ -263,6 +281,7 @@ impl std::fmt::Debug for DataValue {
DataValue::Cred(_) => write!(f, "DataValue::Cred(_)"), DataValue::Cred(_) => write!(f, "DataValue::Cred(_)"),
DataValue::SshKey(_) => write!(f, "DataValue::SshKey(_)"), DataValue::SshKey(_) => write!(f, "DataValue::SshKey(_)"),
DataValue::SecretValue(_) => write!(f, "DataValue::SecretValue(_)"), DataValue::SecretValue(_) => write!(f, "DataValue::SecretValue(_)"),
DataValue::OauthScopeMap(_) => write!(f, "DataValue::OauthScopeMap(_)"),
} }
} }
} }
@ -297,6 +316,8 @@ pub enum PartialValue {
DateTime(OffsetDateTime), DateTime(OffsetDateTime),
EmailAddress(String), EmailAddress(String),
Url(Url), Url(Url),
OauthScope(String),
OauthScopeMap(Uuid),
} }
impl From<SyntaxType> for PartialValue { impl From<SyntaxType> for PartialValue {
@ -590,6 +611,29 @@ impl PartialValue {
matches!(self, PartialValue::Url(_)) matches!(self, PartialValue::Url(_))
} }
pub fn new_oauthscope(s: &str) -> Self {
PartialValue::OauthScope(s.to_string())
}
pub fn is_oauthscope(&self) -> bool {
matches!(self, PartialValue::OauthScope(_))
}
pub fn new_oauthscopemap(u: Uuid) -> Self {
PartialValue::OauthScopeMap(u)
}
pub fn new_oauthscopemap_s(us: &str) -> Option<Self> {
match Uuid::parse_str(us) {
Ok(u) => Some(PartialValue::OauthScopeMap(u)),
Err(_) => None,
}
}
pub fn is_oauthscopemap(&self) -> bool {
matches!(self, PartialValue::OauthScopeMap(_))
}
pub fn to_str(&self) -> Option<&str> { pub fn to_str(&self) -> Option<&str> {
match self { match self {
PartialValue::Utf8(s) => Some(s.as_str()), PartialValue::Utf8(s) => Some(s.as_str()),
@ -652,6 +696,8 @@ impl PartialValue {
odt.format(time::Format::Rfc3339) odt.format(time::Format::Rfc3339)
} }
PartialValue::Url(u) => u.to_string(), PartialValue::Url(u) => u.to_string(),
PartialValue::OauthScope(u) => u.to_string(),
PartialValue::OauthScopeMap(u) => u.to_hyphenated_ref().to_string(),
} }
} }
@ -1191,6 +1237,28 @@ impl Value {
self.pv.is_url() self.pv.is_url()
} }
pub fn new_oauthscope(s: &str) -> Self {
Value {
pv: PartialValue::new_oauthscope(s),
data: None,
}
}
pub fn is_oauthscope(&self) -> bool {
self.pv.is_oauthscope()
}
pub fn new_oauthscopemap(u: Uuid, m: BTreeSet<String>) -> Self {
Value {
pv: PartialValue::new_oauthscopemap(u),
data: Some(Box::new(DataValue::OauthScopeMap(m))),
}
}
pub fn is_oauthscopemap(&self) -> bool {
self.pv.is_oauthscopemap()
}
pub fn lessthan(&self, s: &PartialValue) -> bool { pub fn lessthan(&self, s: &PartialValue) -> bool {
self.pv.lessthan(s) self.pv.lessthan(s)
} }
@ -1294,6 +1362,16 @@ impl Value {
pv: PartialValue::Url(u), pv: PartialValue::Url(u),
data: None, data: None,
}), }),
DbValueV1::OauthScope(s) => Ok(Value {
pv: PartialValue::OauthScope(s),
data: None,
}),
DbValueV1::OauthScopeMap(osm) => Ok(Value {
pv: PartialValue::OauthScopeMap(osm.refer),
data: Some(Box::new(DataValue::OauthScopeMap(
osm.data.into_iter().collect(),
))),
}),
} }
} }
@ -1369,6 +1447,17 @@ impl Value {
DbValueV1::EmailAddress(DbValueEmailAddressV1 { d: mail.clone() }) DbValueV1::EmailAddress(DbValueEmailAddressV1 { d: mail.clone() })
} }
PartialValue::Url(u) => DbValueV1::Url(u.clone()), PartialValue::Url(u) => DbValueV1::Url(u.clone()),
PartialValue::OauthScope(s) => DbValueV1::OauthScope(s.clone()),
PartialValue::OauthScopeMap(u) => {
let data = match &self.data {
Some(v) => match v.as_ref() {
DataValue::OauthScopeMap(m) => m.iter().cloned().collect(),
_ => unreachable!(),
},
None => unreachable!(),
};
DbValueV1::OauthScopeMap(DbValueOauthScopeMapV1 { refer: *u, data })
}
} }
} }
@ -1402,6 +1491,7 @@ impl Value {
pub fn to_ref_uuid(&self) -> Option<&Uuid> { pub fn to_ref_uuid(&self) -> Option<&Uuid> {
match &self.pv { match &self.pv {
PartialValue::Refer(u) => Some(u), PartialValue::Refer(u) => Some(u),
PartialValue::OauthScopeMap(u) => Some(u),
_ => None, _ => None,
} }
} }
@ -1517,6 +1607,20 @@ impl Value {
} }
} }
pub fn to_oauthscope(self) -> Option<String> {
match self.pv {
PartialValue::OauthScope(s) => Some(s),
_ => None,
}
}
pub fn to_oauthscopemap(self) -> Option<(Uuid, BTreeSet<String>)> {
match (self.pv, self.data.map(|b| (*b).clone())) {
(PartialValue::OauthScopeMap(u), Some(DataValue::OauthScopeMap(m))) => Some((u, m)),
_ => None,
}
}
pub fn migrate_iutf8_iname(self) -> Option<Self> { pub fn migrate_iutf8_iname(self) -> Option<Self> {
match self.pv { match self.pv {
PartialValue::Iutf8(v) => Some(Value { PartialValue::Iutf8(v) => Some(Value {
@ -1580,6 +1684,16 @@ impl Value {
odt.format(time::Format::Rfc3339) odt.format(time::Format::Rfc3339)
} }
PartialValue::Url(u) => u.to_string(), PartialValue::Url(u) => u.to_string(),
PartialValue::OauthScope(s) => s.to_string(),
PartialValue::OauthScopeMap(u) => match &self.data {
Some(v) => match v.as_ref() {
DataValue::OauthScopeMap(m) => {
format!("{}: {:?}", u, m)
}
_ => format!("{}: corrupted value tag", u),
},
None => format!("{}: corrupted value", u),
},
} }
} }
@ -1617,6 +1731,14 @@ impl Value {
PartialValue::DateTime(odt) => odt.offset() == time::UtcOffset::UTC, PartialValue::DateTime(odt) => odt.offset() == time::UtcOffset::UTC,
PartialValue::EmailAddress(mail) => validator::validate_email(mail.as_str()), PartialValue::EmailAddress(mail) => validator::validate_email(mail.as_str()),
// PartialValue::Url validated through parsing. // PartialValue::Url validated through parsing.
PartialValue::OauthScope(s) => OAUTHSCOPE_RE.is_match(s),
PartialValue::OauthScopeMap(_) => match &self.data {
Some(v) => match v.as_ref() {
DataValue::OauthScopeMap(m) => m.iter().all(|s| OAUTHSCOPE_RE.is_match(s)),
_ => false,
},
None => false,
},
_ => true, _ => true,
} }
} }
@ -1629,7 +1751,7 @@ impl Value {
| PartialValue::Iname(s) | PartialValue::Iname(s)
| PartialValue::Nsuniqueid(s) | PartialValue::Nsuniqueid(s)
| PartialValue::EmailAddress(s) => vec![s.clone()], | PartialValue::EmailAddress(s) => vec![s.clone()],
PartialValue::Refer(u) | PartialValue::Uuid(u) => { PartialValue::Refer(u) | PartialValue::Uuid(u) | PartialValue::OauthScopeMap(u) => {
vec![u.to_hyphenated_ref().to_string()] vec![u.to_hyphenated_ref().to_string()]
} }
PartialValue::Bool(b) => vec![b.to_string()], PartialValue::Bool(b) => vec![b.to_string()],
@ -1651,6 +1773,7 @@ impl Value {
vec![odt.format(time::Format::Rfc3339)] vec![odt.format(time::Format::Rfc3339)]
} }
PartialValue::Url(u) => vec![u.to_string()], PartialValue::Url(u) => vec![u.to_string()],
PartialValue::OauthScope(_) => vec![],
} }
} }
} }

View file

@ -12,7 +12,8 @@ use time::OffsetDateTime;
use tracing::trace; use tracing::trace;
use crate::be::dbvalue::{ use crate::be::dbvalue::{
DbCidV1, DbValueCredV1, DbValueEmailAddressV1, DbValueTaggedStringV1, DbValueV1, DbCidV1, DbValueCredV1, DbValueEmailAddressV1, DbValueOauthScopeMapV1, DbValueTaggedStringV1,
DbValueV1,
}; };
use crate::value::DataValue; use crate::value::DataValue;
@ -22,7 +23,6 @@ use crate::value::DataValue;
enum I { enum I {
Utf8(BTreeSet<String>), Utf8(BTreeSet<String>),
Iutf8(BTreeSet<String>), Iutf8(BTreeSet<String>),
// Could be AttrString?
Iname(BTreeSet<String>), Iname(BTreeSet<String>),
Uuid(BTreeSet<Uuid>), Uuid(BTreeSet<Uuid>),
Bool(SmolSet<[bool; 1]>), Bool(SmolSet<[bool; 1]>),
@ -40,6 +40,8 @@ enum I {
DateTime(SmolSet<[OffsetDateTime; 1]>), DateTime(SmolSet<[OffsetDateTime; 1]>),
EmailAddress(BTreeSet<String>), EmailAddress(BTreeSet<String>),
Url(SmolSet<[Url; 1]>), Url(SmolSet<[Url; 1]>),
OauthScope(BTreeSet<String>),
OauthScopeMap(BTreeMap<Uuid, BTreeSet<String>>),
} }
pub struct ValueSet { pub struct ValueSet {
@ -110,6 +112,11 @@ impl ValueSet {
PartialValue::DateTime(dt) => I::DateTime(smolset![dt]), PartialValue::DateTime(dt) => I::DateTime(smolset![dt]),
PartialValue::EmailAddress(e) => I::EmailAddress(btreeset![e]), PartialValue::EmailAddress(e) => I::EmailAddress(btreeset![e]),
PartialValue::Url(u) => I::Url(smolset![u]), PartialValue::Url(u) => I::Url(smolset![u]),
PartialValue::OauthScope(x) => I::OauthScope(btreeset![x]),
PartialValue::OauthScopeMap(u) => match data.map(|b| (*b).clone()) {
Some(DataValue::OauthScopeMap(c)) => I::OauthScopeMap(btreemap![(u, c)]),
_ => unreachable!(),
},
}, },
} }
} }
@ -167,6 +174,20 @@ impl ValueSet {
(I::DateTime(set), PartialValue::DateTime(dt)) => Ok(set.insert(dt)), (I::DateTime(set), PartialValue::DateTime(dt)) => Ok(set.insert(dt)),
(I::EmailAddress(set), PartialValue::EmailAddress(e)) => Ok(set.insert(e)), (I::EmailAddress(set), PartialValue::EmailAddress(e)) => Ok(set.insert(e)),
(I::Url(set), PartialValue::Url(u)) => Ok(set.insert(u)), (I::Url(set), PartialValue::Url(u)) => Ok(set.insert(u)),
(I::OauthScope(set), PartialValue::OauthScope(u)) => Ok(set.insert(u)),
(I::OauthScopeMap(map), PartialValue::OauthScopeMap(u)) => {
if let BTreeEntry::Vacant(e) = map.entry(u) {
match data.map(|b| (*b).clone()) {
Some(DataValue::OauthScopeMap(k)) => Ok({
e.insert(k);
true
}),
_ => Err(OperationError::InvalidValueState),
}
} else {
Ok(false)
}
}
(_, _) => Err(OperationError::InvalidValueState), (_, _) => Err(OperationError::InvalidValueState),
} }
} }
@ -242,6 +263,12 @@ impl ValueSet {
(I::Url(a), I::Url(b)) => { (I::Url(a), I::Url(b)) => {
mergesets!(a, b) mergesets!(a, b)
} }
(I::OauthScope(a), I::OauthScope(b)) => {
mergesets!(a, b)
}
(I::OauthScopeMap(a), I::OauthScopeMap(b)) => {
mergemaps!(a, b)
}
// I think that in this case, we need to specify self / everything as we are changing // I think that in this case, we need to specify self / everything as we are changing
// type and we need to potentially purge everything, so we just return the left side. // type and we need to potentially purge everything, so we just return the left side.
_ => Err(OperationError::InvalidValueState), _ => Err(OperationError::InvalidValueState),
@ -328,6 +355,12 @@ impl ValueSet {
set.insert(i); set.insert(i);
}); });
} }
I::OauthScope(set) => {
set.extend(iter.filter_map(|v| v.to_oauthscope()));
}
I::OauthScopeMap(map) => {
map.extend(iter.filter_map(|v| v.to_oauthscopemap()));
}
} }
} }
@ -390,6 +423,12 @@ impl ValueSet {
I::Url(set) => { I::Url(set) => {
set.clear(); set.clear();
} }
I::OauthScope(set) => {
set.clear();
}
I::OauthScopeMap(map) => {
map.clear();
}
}; };
debug_assert!(self.is_empty()); debug_assert!(self.is_empty());
} }
@ -454,6 +493,13 @@ impl ValueSet {
(I::Url(set), PartialValue::Url(u)) => { (I::Url(set), PartialValue::Url(u)) => {
set.remove(u); set.remove(u);
} }
(I::OauthScope(set), PartialValue::OauthScope(u)) => {
set.remove(u);
}
(I::OauthScopeMap(set), PartialValue::OauthScopeMap(u))
| (I::OauthScopeMap(set), PartialValue::Refer(u)) => {
set.remove(u);
}
(_, _) => { (_, _) => {
debug_assert!(false) debug_assert!(false)
} }
@ -484,6 +530,9 @@ impl ValueSet {
(I::DateTime(set), PartialValue::DateTime(dt)) => set.contains(dt), (I::DateTime(set), PartialValue::DateTime(dt)) => set.contains(dt),
(I::EmailAddress(set), PartialValue::EmailAddress(e)) => set.contains(e.as_str()), (I::EmailAddress(set), PartialValue::EmailAddress(e)) => set.contains(e.as_str()),
(I::Url(set), PartialValue::Url(u)) => set.contains(u), (I::Url(set), PartialValue::Url(u)) => set.contains(u),
(I::OauthScope(set), PartialValue::OauthScope(u)) => set.contains(u),
(I::OauthScopeMap(map), PartialValue::OauthScopeMap(u))
| (I::OauthScopeMap(map), PartialValue::Refer(u)) => map.contains_key(u),
_ => false, _ => false,
} }
} }
@ -526,6 +575,8 @@ impl ValueSet {
I::DateTime(set) => set.len(), I::DateTime(set) => set.len(),
I::EmailAddress(set) => set.len(), I::EmailAddress(set) => set.len(),
I::Url(set) => set.len(), I::Url(set) => set.len(),
I::OauthScope(set) => set.len(),
I::OauthScopeMap(set) => set.len(),
} }
} }
@ -574,6 +625,13 @@ impl ValueSet {
I::EmailAddress(set) => set.iter().cloned().collect(), I::EmailAddress(set) => set.iter().cloned().collect(),
// Don't you dare comment on this quinn, it's a URL not a str. // Don't you dare comment on this quinn, it's a URL not a str.
I::Url(set) => set.iter().map(|u| u.to_string()).collect(), I::Url(set) => set.iter().map(|u| u.to_string()).collect(),
// Should we index this?
// I::OauthScope(set) => set.iter().map(|u| u.to_string()).collect(),
I::OauthScope(_set) => vec![],
I::OauthScopeMap(map) => map
.keys()
.map(|u| u.to_hyphenated_ref().to_string())
.collect(),
} }
} }
@ -756,6 +814,30 @@ impl ValueSet {
Some(ValueSet { inner: I::Url(x) }) Some(ValueSet { inner: I::Url(x) })
} }
} }
(I::OauthScope(a), I::OauthScope(b)) => {
let x: BTreeSet<_> = a.difference(b).cloned().collect();
if x.is_empty() {
None
} else {
Some(ValueSet {
inner: I::OauthScope(x),
})
}
}
(I::OauthScopeMap(a), I::OauthScopeMap(b)) => {
let x: BTreeMap<_, _> = a
.iter()
.filter(|(k, _)| b.contains_key(k))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
if x.is_empty() {
None
} else {
Some(ValueSet {
inner: I::OauthScopeMap(x),
})
}
}
// I think that in this case, we need to specify self / everything as we are changing // I think that in this case, we need to specify self / everything as we are changing
// type and we need to potentially purge everything, so we just return the left side. // type and we need to potentially purge everything, so we just return the left side.
_ => Some(self.clone()), _ => Some(self.clone()),
@ -858,6 +940,17 @@ impl ValueSet {
.map(|s| s.as_str()) .map(|s| s.as_str())
.map(Value::new_email_address_s), .map(Value::new_email_address_s),
I::Url(set) => set.iter().take(1).next().cloned().map(Value::new_url), I::Url(set) => set.iter().take(1).next().cloned().map(Value::new_url),
I::OauthScope(set) => set
.iter()
.take(1)
.next()
.map(|s| s.as_str())
.map(Value::new_oauthscope),
I::OauthScopeMap(map) => map
.iter()
.take(1)
.next()
.map(|(u, s)| Value::new_oauthscopemap(*u, s.clone())),
} }
} }
@ -882,6 +975,19 @@ impl ValueSet {
} }
} }
pub fn to_refer_single(&self) -> Option<&Uuid> {
match &self.inner {
I::Refer(set) => {
if set.len() == 1 {
set.iter().take(1).next()
} else {
None
}
}
_ => None,
}
}
pub fn to_bool_single(&self) -> Option<bool> { pub fn to_bool_single(&self) -> Option<bool> {
match &self.inner { match &self.inner {
I::Bool(set) => { I::Bool(set) => {
@ -1022,9 +1128,10 @@ impl ValueSet {
} }
// Value::Refer // Value::Refer
pub fn as_ref_uuid_iter(&self) -> Option<impl Iterator<Item = &Uuid>> { pub fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = &Uuid> + '_>> {
match &self.inner { match &self.inner {
I::Refer(set) => Some(set.iter()), I::Refer(set) => Some(Box::new(set.iter())),
I::OauthScopeMap(map) => Some(Box::new(map.keys())),
_ => None, _ => None,
} }
} }
@ -1036,6 +1143,20 @@ impl ValueSet {
} }
} }
pub fn as_oauthscope_iter(&self) -> Option<impl Iterator<Item = &str>> {
match &self.inner {
I::OauthScope(set) => Some(set.iter().map(|s| s.as_str())),
_ => None,
}
}
pub fn as_oauthscopemap(&self) -> Option<&BTreeMap<Uuid, BTreeSet<String>>> {
match &self.inner {
I::OauthScopeMap(map) => Some(map),
_ => None,
}
}
pub fn to_proto_string_clone_iter(&self) -> ProtoIter<'_> { pub fn to_proto_string_clone_iter(&self) -> ProtoIter<'_> {
// to_proto_string_clone // to_proto_string_clone
match &self.inner { match &self.inner {
@ -1058,6 +1179,8 @@ impl ValueSet {
I::DateTime(set) => ProtoIter::DateTime(set.iter()), I::DateTime(set) => ProtoIter::DateTime(set.iter()),
I::EmailAddress(set) => ProtoIter::EmailAddress(set.iter()), I::EmailAddress(set) => ProtoIter::EmailAddress(set.iter()),
I::Url(set) => ProtoIter::Url(set.iter()), I::Url(set) => ProtoIter::Url(set.iter()),
I::OauthScope(set) => ProtoIter::OauthScope(set.iter()),
I::OauthScopeMap(set) => ProtoIter::OauthScopeMap(set.iter()),
} }
} }
@ -1082,6 +1205,8 @@ impl ValueSet {
I::DateTime(set) => DbValueV1Iter::DateTime(set.iter()), I::DateTime(set) => DbValueV1Iter::DateTime(set.iter()),
I::EmailAddress(set) => DbValueV1Iter::EmailAddress(set.iter()), I::EmailAddress(set) => DbValueV1Iter::EmailAddress(set.iter()),
I::Url(set) => DbValueV1Iter::Url(set.iter()), I::Url(set) => DbValueV1Iter::Url(set.iter()),
I::OauthScope(set) => DbValueV1Iter::OauthScope(set.iter()),
I::OauthScopeMap(set) => DbValueV1Iter::OauthScopeMap(set.iter()),
} }
} }
@ -1106,6 +1231,8 @@ impl ValueSet {
I::DateTime(set) => PartialValueIter::DateTime(set.iter()), I::DateTime(set) => PartialValueIter::DateTime(set.iter()),
I::EmailAddress(set) => PartialValueIter::EmailAddress(set.iter()), I::EmailAddress(set) => PartialValueIter::EmailAddress(set.iter()),
I::Url(set) => PartialValueIter::Url(set.iter()), I::Url(set) => PartialValueIter::Url(set.iter()),
I::OauthScope(set) => PartialValueIter::OauthScope(set.iter()),
I::OauthScopeMap(set) => PartialValueIter::OauthScopeMap(set.iter()),
} }
} }
@ -1130,6 +1257,8 @@ impl ValueSet {
I::DateTime(set) => ValueIter::DateTime(set.iter()), I::DateTime(set) => ValueIter::DateTime(set.iter()),
I::EmailAddress(set) => ValueIter::EmailAddress(set.iter()), I::EmailAddress(set) => ValueIter::EmailAddress(set.iter()),
I::Url(set) => ValueIter::Url(set.iter()), I::Url(set) => ValueIter::Url(set.iter()),
I::OauthScope(set) => ValueIter::OauthScope(set.iter()),
I::OauthScopeMap(set) => ValueIter::OauthScopeMap(set.iter()),
} }
} }
@ -1230,6 +1359,14 @@ impl ValueSet {
matches!(self.inner, I::Url(_)) matches!(self.inner, I::Url(_))
} }
pub fn is_oauthscope(&self) -> bool {
matches!(self.inner, I::OauthScope(_))
}
pub fn is_oauthscopemap(&self) -> bool {
matches!(self.inner, I::OauthScopeMap(_))
}
pub fn migrate_iutf8_iname(&mut self) -> Result<(), OperationError> { pub fn migrate_iutf8_iname(&mut self) -> Result<(), OperationError> {
// Swap iutf8 to Iname internally. // Swap iutf8 to Iname internally.
let ninner = match &self.inner { let ninner = match &self.inner {
@ -1269,6 +1406,8 @@ impl PartialEq for ValueSet {
(I::DateTime(a), I::DateTime(b)) => a.eq(b), (I::DateTime(a), I::DateTime(b)) => a.eq(b),
(I::EmailAddress(a), I::EmailAddress(b)) => a.eq(b), (I::EmailAddress(a), I::EmailAddress(b)) => a.eq(b),
(I::Url(a), I::Url(b)) => a.eq(b), (I::Url(a), I::Url(b)) => a.eq(b),
(I::OauthScope(a), I::OauthScope(b)) => a.eq(b),
(I::OauthScopeMap(a), I::OauthScopeMap(b)) => a.eq(b),
_ => false, _ => false,
} }
} }
@ -1329,6 +1468,8 @@ pub enum ValueIter<'a> {
DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>), DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>),
EmailAddress(std::collections::btree_set::Iter<'a, String>), EmailAddress(std::collections::btree_set::Iter<'a, String>),
Url(SmolSetIter<'a, [Url; 1]>), Url(SmolSetIter<'a, [Url; 1]>),
OauthScope(std::collections::btree_set::Iter<'a, String>),
OauthScopeMap(std::collections::btree_map::Iter<'a, Uuid, BTreeSet<String>>),
} }
impl<'a> Iterator for ValueIter<'a> { impl<'a> Iterator for ValueIter<'a> {
@ -1372,6 +1513,10 @@ impl<'a> Iterator for ValueIter<'a> {
iter.next().map(|i| Value::new_email_address_s(i.as_str())) iter.next().map(|i| Value::new_email_address_s(i.as_str()))
} }
ValueIter::Url(iter) => iter.next().map(|i| Value::from(i.clone())), ValueIter::Url(iter) => iter.next().map(|i| Value::from(i.clone())),
ValueIter::OauthScope(iter) => iter.next().map(|i| Value::new_oauthscope(i)),
ValueIter::OauthScopeMap(iter) => iter
.next()
.map(|(group, scopes)| Value::new_oauthscopemap(*group, scopes.clone())),
} }
} }
} }
@ -1396,6 +1541,8 @@ pub enum PartialValueIter<'a> {
DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>), DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>),
EmailAddress(std::collections::btree_set::Iter<'a, String>), EmailAddress(std::collections::btree_set::Iter<'a, String>),
Url(SmolSetIter<'a, [Url; 1]>), Url(SmolSetIter<'a, [Url; 1]>),
OauthScope(std::collections::btree_set::Iter<'a, String>),
OauthScopeMap(std::collections::btree_map::Iter<'a, Uuid, BTreeSet<String>>),
} }
impl<'a> Iterator for PartialValueIter<'a> { impl<'a> Iterator for PartialValueIter<'a> {
@ -1447,6 +1594,12 @@ impl<'a> Iterator for PartialValueIter<'a> {
.next() .next()
.map(|i| PartialValue::new_email_address_s(i.as_str())), .map(|i| PartialValue::new_email_address_s(i.as_str())),
PartialValueIter::Url(iter) => iter.next().map(|i| PartialValue::from(i.clone())), PartialValueIter::Url(iter) => iter.next().map(|i| PartialValue::from(i.clone())),
PartialValueIter::OauthScope(iter) => {
iter.next().map(|i| PartialValue::new_oauthscope(i))
}
PartialValueIter::OauthScopeMap(iter) => iter
.next()
.map(|(group, _scopes)| PartialValue::new_oauthscopemap(*group)),
} }
} }
} }
@ -1471,6 +1624,8 @@ pub enum DbValueV1Iter<'a> {
DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>), DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>),
EmailAddress(std::collections::btree_set::Iter<'a, String>), EmailAddress(std::collections::btree_set::Iter<'a, String>),
Url(SmolSetIter<'a, [Url; 1]>), Url(SmolSetIter<'a, [Url; 1]>),
OauthScope(std::collections::btree_set::Iter<'a, String>),
OauthScopeMap(std::collections::btree_map::Iter<'a, Uuid, BTreeSet<String>>),
} }
impl<'a> Iterator for DbValueV1Iter<'a> { impl<'a> Iterator for DbValueV1Iter<'a> {
@ -1529,6 +1684,15 @@ impl<'a> Iterator for DbValueV1Iter<'a> {
.next() .next()
.map(|i| DbValueV1::EmailAddress(DbValueEmailAddressV1 { d: i.clone() })), .map(|i| DbValueV1::EmailAddress(DbValueEmailAddressV1 { d: i.clone() })),
DbValueV1Iter::Url(iter) => iter.next().map(|i| DbValueV1::Url(i.clone())), DbValueV1Iter::Url(iter) => iter.next().map(|i| DbValueV1::Url(i.clone())),
DbValueV1Iter::OauthScope(iter) => {
iter.next().map(|i| DbValueV1::OauthScope(i.clone()))
}
DbValueV1Iter::OauthScopeMap(iter) => iter.next().map(|(u, m)| {
DbValueV1::OauthScopeMap(DbValueOauthScopeMapV1 {
refer: *u,
data: m.iter().cloned().collect(),
})
}),
} }
} }
} }
@ -1553,6 +1717,8 @@ pub enum ProtoIter<'a> {
DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>), DateTime(SmolSetIter<'a, [OffsetDateTime; 1]>),
EmailAddress(std::collections::btree_set::Iter<'a, String>), EmailAddress(std::collections::btree_set::Iter<'a, String>),
Url(SmolSetIter<'a, [Url; 1]>), Url(SmolSetIter<'a, [Url; 1]>),
OauthScope(std::collections::btree_set::Iter<'a, String>),
OauthScopeMap(std::collections::btree_map::Iter<'a, Uuid, BTreeSet<String>>),
} }
impl<'a> Iterator for ProtoIter<'a> { impl<'a> Iterator for ProtoIter<'a> {
@ -1600,6 +1766,10 @@ impl<'a> Iterator for ProtoIter<'a> {
}), }),
ProtoIter::EmailAddress(iter) => iter.next().cloned(), ProtoIter::EmailAddress(iter) => iter.next().cloned(),
ProtoIter::Url(iter) => iter.next().map(|i| i.to_string()), ProtoIter::Url(iter) => iter.next().map(|i| i.to_string()),
ProtoIter::OauthScope(iter) => iter.next().cloned(),
ProtoIter::OauthScopeMap(iter) => iter
.next()
.map(|(u, m)| format!("{}: {:?}", ValueSet::uuid_to_proto_string(u), m)),
} }
} }
} }