diff --git a/Cargo.toml b/Cargo.toml index a25e9616..75b575b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ tokio-util = { version = "0.7" } tower = { version = "0.5" } tower-http = { version = "0.6", features = ["compression-full", "request-id", "sensitive-headers", "trace", "util"] } tracing = { version = "0.1" } -tracing-subscriber = { version = "0.3" } +tracing-subscriber = { version = "0.3", features = [] } url = { version = "2.5", features = ["serde"] } utoipa = { version = "5.4", features = ["axum_extras", "chrono"] } utoipa-axum = { version = "0.2" } diff --git a/justfile b/justfile index 163e9f5d..a026f35d 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,5 @@ POLICY_ENTRY_POINTS := \ +" -e identity/validate_token" +\ " -e identity/identity_provider_list" +\ " -e identity/identity_provider_show" +\ " -e identity/identity_provider_create" +\ diff --git a/policy/federation/token/check.rego b/policy/federation/token/check.rego new file mode 100644 index 00000000..27ac6ec1 --- /dev/null +++ b/policy/federation/token/check.rego @@ -0,0 +1,24 @@ +package identity.check_token + +import data.identity + +# Update mapping. + +default allow := false + +allow if { + "admin" in input.credentials.roles +} + +allow if { + "reader" in input.credentials.roles + "all" in input.credentials.system_scope +} + +allow if { + identity.token_subject +} + +allow if { + "service" in input.credentials.roles +} diff --git a/policy/federation/token/validate.rego b/policy/federation/token/validate.rego new file mode 100644 index 00000000..3dcecb05 --- /dev/null +++ b/policy/federation/token/validate.rego @@ -0,0 +1,24 @@ +package identity.validate_token + +import data.identity + +# Update mapping. + +default allow := false + +allow if { + "admin" in input.credentials.roles +} + +allow if { + "reader" in input.credentials.roles + "all" in input.credentials.system_scope +} + +allow if { + identity.token_subject +} + +allow if { + "service" in input.credentials.roles +} diff --git a/policy/identity.rego b/policy/identity.rego index f7149fc8..c13a7c8d 100644 --- a/policy/identity.rego +++ b/policy/identity.rego @@ -1,5 +1,9 @@ package identity +token_subject if { + input.credentials.user_id == input.target.token.user_id +} + global_idp if { not input.target.domain_id } diff --git a/src/api/mod.rs b/src/api/mod.rs index 838f8b55..7a77c56c 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -19,7 +19,9 @@ use axum::{ }; use utoipa::{ Modify, OpenApi, - openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, + openapi::security::{ + ApiKey, ApiKeyValue, AuthorizationCode, Flow, OAuth2, Scopes, SecurityScheme, + }, }; use utoipa_axum::{router::OpenApiRouter, routes}; @@ -54,7 +56,18 @@ impl Modify for SecurityAddon { components.add_security_scheme( "x-auth", SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("x-auth-token"))), - ) + ); + // TODO: This must be dynamic + components.add_security_scheme( + "oauth2", + SecurityScheme::OAuth2(OAuth2::new([Flow::AuthorizationCode( + AuthorizationCode::new( + "https://localhost/authorization/token", + "https://localhost/token/url", + Scopes::from_iter([("openid", "default scope")]), + ), + )])), + ); } } } diff --git a/src/api/v3/auth/token/mod.rs b/src/api/v3/auth/token/mod.rs index 644bed7d..a0d7bf25 100644 --- a/src/api/v3/auth/token/mod.rs +++ b/src/api/v3/auth/token/mod.rs @@ -19,6 +19,9 @@ use axum::{ http::StatusCode, response::IntoResponse, }; +use mockall_double::double; +use serde_json::{json, to_value}; +use tracing::error; use utoipa_axum::{router::OpenApiRouter, routes}; use crate::api::types::Scope; @@ -36,12 +39,14 @@ use crate::auth::{AuthenticatedInfo, AuthzInfo}; use crate::catalog::CatalogApi; use crate::identity::IdentityApi; use crate::keystone::ServiceState; +#[double] +use crate::policy::Policy; use crate::token::TokenApi; mod common; pub mod types; -pub(super) fn openapi_router() -> OpenApiRouter { +pub(crate) fn openapi_router() -> OpenApiRouter { OpenApiRouter::new().routes(routes!(show, post)) } @@ -159,8 +164,6 @@ async fn post( let authed_info = authenticate_request(&state, &req).await?; let authz_info = get_authz_info(&state, &req).await?; - println!("Authz info is {:?}", authz_info); - let mut token = state .provider .get_token_provider() @@ -209,10 +212,11 @@ async fn post( #[tracing::instrument( name = "api::token_get", level = "debug", - skip(state, headers, _user_auth) + skip(state, headers, user_auth, policy) )] async fn show( - Auth(_user_auth): Auth, + Auth(user_auth): Auth, + mut policy: Policy, Query(query): Query, headers: HeaderMap, State(state): State, @@ -224,16 +228,28 @@ async fn show( .map_err(|_| KeystoneApiError::InvalidHeader)? .to_string(); + // Default behavior is to return 404 for expired tokens. It makes sense to log internally the + // error before mapping it. let mut token = state .provider .get_token_provider() .validate_token(&subject_token, query.allow_expired, None) .await + .inspect_err(|e| error!("{:?}", e.to_string())) .map_err(|_| KeystoneApiError::NotFound { resource: "token".into(), identifier: String::new(), })?; + policy + .enforce( + "identity/validate_token", + &user_auth, + to_value(json!({"token": &token}))?, + None, + ) + .await?; + token = state .provider .get_token_provider() @@ -283,7 +299,7 @@ mod tests { types::{UserPasswordAuthRequest, UserResponse}, }; use crate::keystone::Service; - use crate::policy::MockPolicyFactory; + use crate::policy::{MockPolicy, MockPolicyFactory, PolicyEvaluationResult}; use crate::provider::Provider; use crate::resource::{ MockResourceProvider, @@ -297,6 +313,18 @@ mod tests { use super::*; + fn get_policy_factory_mock() -> MockPolicyFactory { + let mut policy_factory_mock = MockPolicyFactory::default(); + policy_factory_mock.expect_instantiate().returning(|| { + let mut policy_mock = MockPolicy::default(); + policy_mock + .expect_enforce() + .returning(|_, _, _, _| Ok(PolicyEvaluationResult::allowed())); + Ok(policy_mock) + }); + policy_factory_mock + } + #[tokio::test] async fn test_authenticate_request_password() { let config = Config::default(); @@ -534,7 +562,7 @@ mod tests { Config::default(), DatabaseConnection::Disconnected, provider, - MockPolicyFactory::new(), + get_policy_factory_mock(), ) .unwrap(), ); @@ -647,7 +675,7 @@ mod tests { Config::default(), DatabaseConnection::Disconnected, provider, - MockPolicyFactory::new(), + get_policy_factory_mock(), ) .unwrap(), ); @@ -708,7 +736,7 @@ mod tests { Config::default(), DatabaseConnection::Disconnected, provider, - MockPolicyFactory::new(), + get_policy_factory_mock(), ) .unwrap(), ); diff --git a/src/api/v4/auth/token/mod.rs b/src/api/v4/auth/token/mod.rs index 2c3effa3..f587b9b2 100644 --- a/src/api/v4/auth/token/mod.rs +++ b/src/api/v4/auth/token/mod.rs @@ -11,38 +11,27 @@ // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 - -use axum::{ - Json, - extract::{Query, State}, - http::HeaderMap, - http::StatusCode, - response::IntoResponse, -}; -use utoipa_axum::{router::OpenApiRouter, routes}; +#![allow(dead_code)] +use utoipa_axum::router::OpenApiRouter; use crate::api::types::Scope; -use crate::api::v4::auth::token::types::{ - AuthRequest, CreateTokenParameters, Token as ApiResponseToken, TokenResponse, - ValidateTokenParameters, -}; +use crate::api::v4::auth::token::types::AuthRequest; use crate::api::{ - Catalog, - auth::Auth, common::{find_project_from_scope, get_domain}, error::KeystoneApiError, }; use crate::auth::{AuthenticatedInfo, AuthzInfo}; -use crate::catalog::CatalogApi; use crate::identity::IdentityApi; use crate::keystone::ServiceState; use crate::token::TokenApi; +use crate::api::v3::auth::token as v3_token; + mod common; pub mod types; pub(super) fn openapi_router() -> OpenApiRouter { - OpenApiRouter::new().routes(routes!(show, post)) + v3_token::openapi_router() } /// Authenticate the user ignoring any scope information. It is important not to expose any @@ -139,123 +128,6 @@ async fn get_authz_info( Ok(authz_info) } -/// Authenticate user issuing a new token -#[utoipa::path( - post, - path = "/", - description = "Issue token", - params(CreateTokenParameters), - responses( - (status = OK, description = "Token object", body = TokenResponse), - ), - tag="auth" -)] -#[tracing::instrument(name = "api::token_post", level = "debug", skip(state, req))] -async fn post( - Query(query): Query, - State(state): State, - Json(req): Json, -) -> Result { - let authed_info = authenticate_request(&state, &req).await?; - let authz_info = get_authz_info(&state, &req).await?; - - let mut token = state - .provider - .get_token_provider() - .issue_token(authed_info, authz_info)?; - - token = state - .provider - .get_token_provider() - .expand_token_information(&token, &state.db, &state.provider) - .await?; - - let mut api_token = TokenResponse { - token: ApiResponseToken::from_provider_token(&state, &token).await?, - }; - if !query.nocatalog.is_some_and(|x| x) { - let catalog: Catalog = state - .provider - .get_catalog_provider() - .get_catalog(&state.db, true) - .await? - .into(); - api_token.token.catalog = Some(catalog); - } - return Ok(( - StatusCode::OK, - [( - "X-Subject-Token", - state.provider.get_token_provider().encode_token(&token)?, - )], - Json(api_token), - ) - .into_response()); -} - -/// Validate token -#[utoipa::path( - get, - path = "/", - description = "Validate token", - params(ValidateTokenParameters), - responses( - (status = OK, description = "Token object", body = TokenResponse), - ), - tag="auth" -)] -#[tracing::instrument( - name = "api::token_get", - level = "debug", - skip(state, headers, _user_auth) -)] -async fn show( - Auth(_user_auth): Auth, - Query(query): Query, - headers: HeaderMap, - State(state): State, -) -> Result { - let subject_token: String = headers - .get("X-Subject-Token") - .ok_or(KeystoneApiError::SubjectTokenMissing)? - .to_str() - .map_err(|_| KeystoneApiError::InvalidHeader)? - .to_string(); - - let mut token = state - .provider - .get_token_provider() - .validate_token(&subject_token, query.allow_expired, None) - .await - .map_err(|_| KeystoneApiError::NotFound { - resource: "token".into(), - identifier: String::new(), - })?; - - token = state - .provider - .get_token_provider() - .expand_token_information(&token, &state.db, &state.provider) - .await - .map_err(|_| KeystoneApiError::Forbidden)?; - - let mut response_token = ApiResponseToken::from_provider_token(&state, &token).await?; - - if !query.nocatalog.is_some_and(|x| x) { - let catalog: Catalog = state - .provider - .get_catalog_provider() - .get_catalog(&state.db, true) - .await? - .into(); - response_token.catalog = Some(catalog); - } - - Ok(TokenResponse { - token: response_token, - }) -} - #[cfg(test)] mod tests { use axum::{ @@ -281,7 +153,7 @@ mod tests { types::{UserPasswordAuthRequest, UserResponse}, }; use crate::keystone::Service; - use crate::policy::MockPolicyFactory; + use crate::policy::{MockPolicy, MockPolicyFactory, PolicyEvaluationResult}; use crate::provider::Provider; use crate::resource::{ MockResourceProvider, @@ -295,6 +167,18 @@ mod tests { use super::*; + fn get_policy_factory_mock() -> MockPolicyFactory { + let mut policy_factory_mock = MockPolicyFactory::default(); + policy_factory_mock.expect_instantiate().returning(|| { + let mut policy_mock = MockPolicy::default(); + policy_mock + .expect_enforce() + .returning(|_, _, _, _| Ok(PolicyEvaluationResult::allowed())); + Ok(policy_mock) + }); + policy_factory_mock + } + #[tokio::test] async fn test_authenticate_request_password() { let config = Config::default(); @@ -532,7 +416,7 @@ mod tests { Config::default(), DatabaseConnection::Disconnected, provider, - MockPolicyFactory::new(), + get_policy_factory_mock(), ) .unwrap(), ); @@ -645,7 +529,7 @@ mod tests { Config::default(), DatabaseConnection::Disconnected, provider, - MockPolicyFactory::new(), + get_policy_factory_mock(), ) .unwrap(), ); @@ -706,7 +590,7 @@ mod tests { Config::default(), DatabaseConnection::Disconnected, provider, - MockPolicyFactory::new(), + get_policy_factory_mock(), ) .unwrap(), ); diff --git a/src/api/v4/federation/auth.rs b/src/api/v4/federation/auth.rs index c61f31b1..00518de8 100644 --- a/src/api/v4/federation/auth.rs +++ b/src/api/v4/federation/auth.rs @@ -39,7 +39,22 @@ pub(super) fn openapi_router() -> OpenApiRouter { OpenApiRouter::new().routes(routes!(post)) } -/// Authenticate using identity provider +/// Authenticate using identity provider. +/// +/// Initiate the authentication for the given identity provider. Mapping can be passed, otherwise +/// the one which is set as a default on the identity provider level is used. +/// +/// The API returns the link to the identity provider which must be open in the web browser. Once +/// user authenticates in the identity provider UI a redirect to the url passed as a callback in +/// the request is being done as a typical oauth2 authorization code callback. The client is +/// responsible for serving this callback server and use received authorization code and state to +/// exchange it for the Keystone token passing it to the `/v4/federation/oidc/callback`. +/// +/// Desired scope (OpenStack) can be also passed to get immediately scoped token after the +/// authentication completes instead of the unscoped token. +/// +/// This is an unauthenticated API call. User, mapping, scope validation will happen when the +/// callback is invoked. #[utoipa::path( post, path = "/identity_providers/{idp_id}/auth", diff --git a/src/api/v4/federation/oidc.rs b/src/api/v4/federation/oidc.rs index 1cf6960a..7f57f82d 100644 --- a/src/api/v4/federation/oidc.rs +++ b/src/api/v4/federation/oidc.rs @@ -86,7 +86,12 @@ async fn get_authz_info( Ok(authz_info) } -/// Authenticate callback +/// Authentication callback. +/// +/// This operation allows user to exchange the authorization code retrieved from the identity +/// provider after calling the `/v4/federation/identity_providers/{idp_id}/auth` for the Keystone +/// token. When desired scope was passed in that auth initialization call the scoped token is +/// returned (assuming the user is having roles assigned on that scope). #[utoipa::path( post, path = "/oidc/callback", @@ -98,6 +103,7 @@ async fn get_authz_info( ), ), ), + security(("oauth2" = ["openid"])), tag="identity_providers" )] #[tracing::instrument( diff --git a/src/api/v4/federation/types/mapping.rs b/src/api/v4/federation/types/mapping.rs index 8eb36415..390c29fa 100644 --- a/src/api/v4/federation/types/mapping.rs +++ b/src/api/v4/federation/types/mapping.rs @@ -332,7 +332,7 @@ impl IntoResponse for MappingList { } } -/// Query parameters for listing OIDC/JWT mappings +/// Query parameters for listing OIDC/JWT mappings. #[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams)] pub struct MappingListParameters { /// Filters the response by IDP name. @@ -340,6 +340,7 @@ pub struct MappingListParameters { /// Filters the response by a domain ID. pub domain_id: Option, + /// Filters the response by a idp ID. pub idp_id: Option, } diff --git a/src/federation/mod.rs b/src/federation/mod.rs index 2077dbb7..052be5c8 100644 --- a/src/federation/mod.rs +++ b/src/federation/mod.rs @@ -350,7 +350,7 @@ impl FederationApi for FederationProvider { id: &'a str, idp: MappingUpdate, ) -> Result { - // TODO: Check update of idp_id to enure it belongs to the same domain + // TODO: Check update of idp_id to ensure it belongs to the same domain self.backend_driver.update_mapping(db, id, idp).await } diff --git a/src/policy.rs b/src/policy.rs index 73481314..172c74fd 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -25,7 +25,7 @@ use std::path::Path; use thiserror::Error; use tokio::io::AsyncRead; use tokio::io::AsyncReadExt; -use tracing::{debug, trace}; +use tracing::{Level, debug}; use crate::token::Token; @@ -47,6 +47,10 @@ pub enum PolicyError { #[error(transparent)] Join(#[from] tokio::task::JoinError), + /// Json serializaion error. + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error(transparent)] Wasm(#[from] opa_wasm::wasmtime::Error), } @@ -103,7 +107,7 @@ impl PolicyFactory { Ok(factory) } - #[tracing::instrument(name = "policy.instantiate", skip_all, err)] + #[tracing::instrument(name = "policy.instantiate", level = Level::TRACE, skip_all, err)] pub async fn instantiate(&self) -> Result { if let (Some(engine), Some(module)) = (&self.engine, &self.module) { let mut store = Store::new(engine, ()); @@ -156,7 +160,7 @@ pub enum EvaluationError { } /// OpenPolicyAgent `Credentials` object -#[derive(Serialize)] +#[derive(Serialize, Debug)] pub struct Credentials { pub user_id: String, pub roles: Vec, @@ -184,31 +188,33 @@ impl Policy { skip_all, fields( entrypoint = policy_name.as_ref(), + input, + result, ), err, + level = Level::DEBUG )] pub async fn enforce>( &mut self, policy_name: P, - credentials: &Token, + credentials: impl Into, target: Value, update: Option, ) -> Result { + let creds: Credentials = credentials.into(); let input = json!({ - "credentials": Credentials::from(credentials), + "credentials": creds, "target": target, "update": update, }); if let (Some(store), Some(instance)) = (&mut self.store, &self.instance) { - trace!( - "enforcing policy {} with target {target}", - policy_name.as_ref() - ); + tracing::Span::current().record("input", serde_json::to_string(&input)?); let [res]: [OpaResponse; 1] = instance .evaluate(store, policy_name.as_ref(), &input) .await?; - debug!("Res is {:?}", res); + tracing::Span::current().record("result", serde_json::to_string(&res.result)?); + debug!("authorized={}", res.result.allow()); if !res.result.allow() { return Err(PolicyError::Forbidden(res.result)); } @@ -225,7 +231,7 @@ impl Policy { } /// A single violation of a policy. -#[derive(Clone, Deserialize, Debug, JsonSchema)] +#[derive(Clone, Deserialize, Debug, JsonSchema, Serialize)] pub struct Violation { pub msg: String, pub field: Option, @@ -238,7 +244,7 @@ pub struct OpaResponse { } /// The result of a policy evaluation. -#[derive(Clone, Deserialize, Debug)] +#[derive(Clone, Deserialize, Debug, Serialize)] pub struct PolicyEvaluationResult { pub allow: bool, #[serde(rename = "violation")] diff --git a/src/token/application_credential.rs b/src/token/application_credential.rs index eb756594..55d1cba8 100644 --- a/src/token/application_credential.rs +++ b/src/token/application_credential.rs @@ -15,6 +15,7 @@ use chrono::{DateTime, Utc}; use derive_builder::Builder; use rmp::{decode::read_pfix, encode::write_pfix}; +use serde::Serialize; use std::collections::BTreeMap; use std::io::Write; @@ -28,7 +29,7 @@ use crate::token::{ types::Token, }; -#[derive(Builder, Clone, Debug, Default, PartialEq)] +#[derive(Builder, Clone, Debug, Default, PartialEq, Serialize)] #[builder(setter(into))] pub struct ApplicationCredentialPayload { pub user_id: String, diff --git a/src/token/domain_scoped.rs b/src/token/domain_scoped.rs index a2e00e7d..7dad9059 100644 --- a/src/token/domain_scoped.rs +++ b/src/token/domain_scoped.rs @@ -15,6 +15,7 @@ use chrono::{DateTime, Utc}; use derive_builder::Builder; use rmp::{decode::read_pfix, encode::write_pfix}; +use serde::Serialize; use std::collections::BTreeMap; use std::io::Write; @@ -28,7 +29,7 @@ use crate::token::{ types::Token, }; -#[derive(Builder, Clone, Debug, Default, PartialEq)] +#[derive(Builder, Clone, Debug, Default, PartialEq, Serialize)] #[builder(setter(into))] pub struct DomainScopePayload { pub user_id: String, diff --git a/src/token/federation_domain_scoped.rs b/src/token/federation_domain_scoped.rs index 391a33ea..31cd6814 100644 --- a/src/token/federation_domain_scoped.rs +++ b/src/token/federation_domain_scoped.rs @@ -15,6 +15,7 @@ use chrono::{DateTime, Utc}; use derive_builder::Builder; use rmp::{decode::read_pfix, encode::write_pfix}; +use serde::Serialize; use std::collections::BTreeMap; use std::io::Write; @@ -29,7 +30,7 @@ use crate::token::{ }; /// Federated domain scope token payload -#[derive(Builder, Clone, Debug, Default, PartialEq)] +#[derive(Builder, Clone, Debug, Default, PartialEq, Serialize)] #[builder(setter(into))] pub struct FederationDomainScopePayload { pub user_id: String, diff --git a/src/token/federation_project_scoped.rs b/src/token/federation_project_scoped.rs index 835bd81b..3456ccb6 100644 --- a/src/token/federation_project_scoped.rs +++ b/src/token/federation_project_scoped.rs @@ -15,6 +15,7 @@ use chrono::{DateTime, Utc}; use derive_builder::Builder; use rmp::{decode::read_pfix, encode::write_pfix}; +use serde::Serialize; use std::collections::BTreeMap; use std::io::Write; @@ -29,7 +30,7 @@ use crate::token::{ }; /// Federated project scope token payload -#[derive(Builder, Clone, Debug, Default, PartialEq)] +#[derive(Builder, Clone, Debug, Default, PartialEq, Serialize)] #[builder(setter(into))] pub struct FederationProjectScopePayload { pub user_id: String, diff --git a/src/token/federation_unscoped.rs b/src/token/federation_unscoped.rs index 5bbd598c..fe82a805 100644 --- a/src/token/federation_unscoped.rs +++ b/src/token/federation_unscoped.rs @@ -15,6 +15,7 @@ use chrono::{DateTime, Utc}; use derive_builder::Builder; use rmp::{decode::read_pfix, encode::write_pfix}; +use serde::Serialize; use std::collections::BTreeMap; use std::io::Write; @@ -27,7 +28,7 @@ use crate::token::{ }; /// Federated unscoped token payload -#[derive(Builder, Clone, Debug, Default, PartialEq)] +#[derive(Builder, Clone, Debug, Default, PartialEq, Serialize)] #[builder(setter(into))] pub struct FederationUnscopedPayload { pub user_id: String, diff --git a/src/token/project_scoped.rs b/src/token/project_scoped.rs index bdfcaddb..aa972812 100644 --- a/src/token/project_scoped.rs +++ b/src/token/project_scoped.rs @@ -15,6 +15,7 @@ use chrono::{DateTime, Utc}; use derive_builder::Builder; use rmp::{decode::read_pfix, encode::write_pfix}; +use serde::Serialize; use std::collections::BTreeMap; use std::io::Write; @@ -28,7 +29,7 @@ use crate::token::{ types::Token, }; -#[derive(Builder, Clone, Debug, Default, PartialEq)] +#[derive(Builder, Clone, Debug, Default, PartialEq, Serialize)] #[builder(setter(into))] pub struct ProjectScopePayload { pub user_id: String, diff --git a/src/token/types.rs b/src/token/types.rs index 12d970c2..a141890d 100644 --- a/src/token/types.rs +++ b/src/token/types.rs @@ -14,6 +14,7 @@ use chrono::{DateTime, Utc}; use dyn_clone::DynClone; +use serde::Serialize; use crate::assignment::types::Role; use crate::config::Config; @@ -28,7 +29,8 @@ use crate::token::federation_unscoped::FederationUnscopedPayload; use crate::token::project_scoped::ProjectScopePayload; use crate::token::unscoped::UnscopedPayload; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(untagged)] pub enum Token { Unscoped(UnscopedPayload), DomainScope(DomainScopePayload), diff --git a/src/token/unscoped.rs b/src/token/unscoped.rs index 9004f278..1a72433e 100644 --- a/src/token/unscoped.rs +++ b/src/token/unscoped.rs @@ -15,6 +15,7 @@ use chrono::{DateTime, Utc}; use derive_builder::Builder; use rmp::{decode::read_pfix, encode::write_pfix}; +use serde::Serialize; use std::collections::BTreeMap; use std::io::Write; @@ -26,7 +27,7 @@ use crate::token::{ types::Token, }; -#[derive(Builder, Clone, Debug, Default, PartialEq)] +#[derive(Builder, Clone, Debug, Default, PartialEq, Serialize)] #[builder(setter(into))] pub struct UnscopedPayload { pub user_id: String,