20221116 oauth2 app portal (#1200)

This commit is contained in:
Firstyear 2022-11-17 10:06:13 +10:00 committed by GitHub
parent 7c07688fa6
commit 40dd911d10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 228 additions and 74 deletions

13
Cargo.lock generated
View file

@ -2505,6 +2505,7 @@ dependencies = [
"serde",
"serde-wasm-bindgen 0.4.5",
"serde_json",
"url",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
@ -5041,9 +5042,9 @@ dependencies = [
[[package]]
name = "webauthn-authenticator-rs"
version = "0.4.8"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc33594bd26aabc90ce6d081d98617f5d16803660187f88912dead3ac2a9784"
checksum = "603b8602cae2d6c3706b6195765ff582389494d10c442d84a1de2ed5a25679ef"
dependencies = [
"authenticator-ctap2-2021",
"base64urlsafedata",
@ -5075,9 +5076,9 @@ dependencies = [
[[package]]
name = "webauthn-rs-core"
version = "0.4.8"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ef9a989b5cd2c39c52850c4bc36ebc88907a06ceacf7f80e45d78a308944b9d"
checksum = "294c78c83f12153a51e1cf1e6970b5da1397645dada39033a9c3173a8fc4fc2b"
dependencies = [
"base64 0.13.1",
"base64urlsafedata",
@ -5099,9 +5100,9 @@ dependencies = [
[[package]]
name = "webauthn-rs-proto"
version = "0.4.8"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c7662de492733e6ea11e2726270bf34f5d7f8dbd08cd089830fbb3639a229"
checksum = "d24e638361a63ba5c0a0be6a60229490fcdf33740ed63df5bb6bdb627b52a138"
dependencies = [
"base64urlsafedata",
"js-sys",

View file

@ -0,0 +1,16 @@
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Debug, Serialize, Deserialize, Clone)]
/// This is a description of a linked or connected application for a user. This is
/// used in the UI to render applications on the dashboard for a user to access.
pub enum AppLink {
Oauth2 {
display_name: String,
redirect_url: Url,
// Future problem.
// icon: Icon,
}
}

View file

@ -14,5 +14,6 @@ pub mod oauth2;
pub mod scim_v1;
pub mod utils;
pub mod v1;
pub mod internal;
pub use webauthn_rs_proto as webauthn;

View file

@ -170,15 +170,23 @@ impl LdapServer {
// Map the Some(a,v) to ...?
let ext_filter = match (&sr.scope, req_dn) {
(LdapSearchScope::OneLevel, Some(_r)) => return Ok(vec![sr.gen_success()]),
(LdapSearchScope::OneLevel, None) => {
// OneLevel and Child searches are veerrrryyy similar for us because child
// is a "subtree search excluding base". Because we don't have a tree structure at
// all, this is the same as a onelevel (ald children of base excludeing base).
(LdapSearchScope::Children, Some(_r)) | (LdapSearchScope::OneLevel, Some(_r)) => {
return Ok(vec![sr.gen_success()])
}
(LdapSearchScope::Children, None) | (LdapSearchScope::OneLevel, None) => {
// exclude domain_info
Some(LdapFilter::Not(Box::new(LdapFilter::Equality(
"uuid".to_string(),
STR_UUID_DOMAIN_INFO.to_string(),
))))
}
(LdapSearchScope::Base, Some((a, v))) => Some(LdapFilter::Equality(a, v)),
// because we request a specific DN, these are the same since we want the same
// entry.
(LdapSearchScope::Base, Some((a, v)))
| (LdapSearchScope::Subtree, Some((a, v))) => Some(LdapFilter::Equality(a, v)),
(LdapSearchScope::Base, None) => {
// domain_info
Some(LdapFilter::Equality(
@ -186,7 +194,6 @@ impl LdapServer {
STR_UUID_DOMAIN_INFO.to_string(),
))
}
(LdapSearchScope::Subtree, Some((a, v))) => Some(LdapFilter::Equality(a, v)),
(LdapSearchScope::Subtree, None) => {
// No filter changes needed.
None

View file

@ -35,6 +35,7 @@ uuid = "^1.2.1"
wasm-bindgen = { version = "^0.2.81" }
wasm-bindgen-futures = { version = "^0.4.30" }
wasm-bindgen-test = "0.3.33"
url = "^2.3.1"
yew = "^0.19.3"
yew-agent = "^0.1.0"
yew-router = "^0.16.0"

View file

@ -1040,16 +1040,16 @@ function getImports() {
const ret = wasm.memory;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper4757 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1013, __wbg_adapter_48);
imports.wbg.__wbindgen_closure_wrapper4782 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1015, __wbg_adapter_48);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper4941 = function(arg0, arg1, arg2) {
const ret = makeClosure(arg0, arg1, 1037, __wbg_adapter_51);
imports.wbg.__wbindgen_closure_wrapper4966 = function(arg0, arg1, arg2) {
const ret = makeClosure(arg0, arg1, 1039, __wbg_adapter_51);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper5597 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1287, __wbg_adapter_54);
imports.wbg.__wbindgen_closure_wrapper5621 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1288, __wbg_adapter_54);
return addHeapObject(ret);
};

View file

@ -4,7 +4,7 @@ use gloo::console;
use yew::{html, Component, Context, Html, Properties};
use yew_router::prelude::Link;
use crate::components::adminmenu::{Entity, EntityType, GetError};
use crate::components::admin_menu::{Entity, EntityType, GetError};
use crate::components::alpha_warning_banner;
use crate::constants::{
CSS_BREADCRUMB_ITEM, CSS_BREADCRUMB_ITEM_ACTIVE, CSS_CELL, CSS_DT, CSS_TABLE,

View file

@ -4,7 +4,7 @@ use gloo::console;
use yew::{html, Component, Context, Html, Properties};
use yew_router::prelude::Link;
use crate::components::adminmenu::{Entity, EntityType, GetError};
use crate::components::admin_menu::{Entity, EntityType, GetError};
use crate::components::alpha_warning_banner;
use crate::constants::{CSS_BREADCRUMB_ITEM, CSS_BREADCRUMB_ITEM_ACTIVE, CSS_CELL, CSS_TABLE};
use crate::utils::{do_alert_error, do_page_header, init_request};

View file

@ -3,13 +3,10 @@ use yew::{html, Component, Context, Html, Properties};
use yew_router::prelude::Link;
use crate::components::alpha_warning_banner;
use crate::constants::{CSS_LINK_DARK_STRETCHED, CSS_PAGE_HEADER};
use crate::constants::{CSS_CARD, CSS_CARD_BODY, CSS_LINK_DARK_STRETCHED, CSS_PAGE_HEADER};
// use crate::error::FetchError;
use crate::views::AdminRoute;
const CSS_CARD: &str = "card text-center";
const CSS_CARD_BODY: &str = "card-body text-center";
#[derive(Eq, PartialEq, Properties)]
pub struct Props;

View file

@ -4,7 +4,7 @@ use gloo::console;
use yew::{html, Component, Context, Html, Properties};
use yew_router::prelude::Link;
use crate::components::adminmenu::{Entity, EntityType, GetError};
use crate::components::admin_menu::{Entity, EntityType, GetError};
use crate::components::alpha_warning_banner;
use crate::constants::{CSS_BREADCRUMB_ITEM, CSS_BREADCRUMB_ITEM_ACTIVE, CSS_CELL, CSS_TABLE};
use crate::utils::{do_alert_error, do_page_header, init_request};

View file

@ -3,8 +3,8 @@ use yew::Html;
pub mod admin_accounts;
pub mod admin_groups;
pub mod admin_menu;
pub mod admin_oauth2;
pub mod adminmenu;
pub mod change_unix_password;
/// creates the "Kanidm is alpha" banner

View file

@ -30,3 +30,7 @@ pub const CSS_DT: &str = "col-6";
pub const CSS_BREADCRUMB_ITEM: &str = "breadcrumb-item";
pub const CSS_BREADCRUMB_ITEM_ACTIVE: &str = "breadcrumb-item active";
// used in the UI for ... cards
pub const CSS_CARD: &str = "card text-center";
pub const CSS_CARD_BODY: &str = "card-body text-center";

View file

@ -244,9 +244,7 @@ impl Component for Oauth2App {
};
}
};
let e_msg = format!("{:?}", query);
console::error!(e_msg.as_str());
console::debug!(format!("{query:?}", ));
// In the query, if this is openid there MAY be a hint
// as to the users name.

View file

@ -3,22 +3,58 @@ use gloo::console;
use yew::prelude::*;
use crate::components::alpha_warning_banner;
use crate::constants::{CSS_CELL, CSS_PAGE_HEADER, CSS_TABLE};
use crate::constants::{CSS_CARD, CSS_LINK_DARK_STRETCHED, CSS_PAGE_HEADER, CSS_CARD_BODY};
use crate::error::FetchError;
use wasm_bindgen::prelude::*;
// use crate::utils;
// use wasm_bindgen::JsCast;
// use wasm_bindgen_futures::JsFuture;
// use web_sys::{Request, RequestCredentials, RequestInit, RequestMode, Response};
use kanidm_proto::internal::AppLink;
pub enum Msg {
// Nothing
Ready { apps: Vec<AppLink> },
Error { emsg: String, kopid: Option<String> },
}
pub struct AppsApp {}
impl From<FetchError> for Msg {
fn from(fe: FetchError) -> Self {
Msg::Error {
emsg: fe.as_string(),
kopid: None,
}
}
}
pub enum State {
Waiting,
Ready { apps: Vec<AppLink> },
Error { emsg: String, kopid: Option<String> },
}
pub struct AppsApp {
state: State,
}
impl Component for AppsApp {
type Message = Msg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
fn create(ctx: &Context<Self>) -> Self {
#[cfg(debug)]
console::debug!("views::apps::create");
AppsApp {}
ctx.link().send_future(async {
match Self::fetch_user_apps().await {
Ok(v) => v,
Err(v) => v.into(),
}
});
let state = State::Waiting;
AppsApp { state }
}
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
@ -27,15 +63,14 @@ impl Component for AppsApp {
false
}
fn update(&mut self, _ctx: &Context<Self>, _msg: Self::Message) -> bool {
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
#[cfg(debug)]
console::debug!("views::apps::update");
/*
match msg {
ViewsMsg::Logout => {
}
Msg::Ready { apps } => self.state = State::Ready { apps },
Msg::Error { emsg, kopid } => self.state = State::Error { emsg, kopid },
}
*/
true
}
@ -44,44 +79,138 @@ impl Component for AppsApp {
console::debug!("views::apps::rendered");
}
fn view(&self, _ctx: &Context<Self>) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
match &self.state {
State::Waiting => self.view_waiting(),
State::Ready { apps } => self.view_ready(ctx, apps.as_slice()),
State::Error { emsg, kopid } => self.view_error(ctx, &emsg, kopid.as_deref()),
}
}
}
impl AppsApp {
fn view_waiting(&self) -> Html {
html! {
<>
<div class={CSS_PAGE_HEADER}>
<h2>{ "Apps" }</h2>
</div>
{ alpha_warning_banner() }
<div class="table-responsive">
<table class={CSS_TABLE}>
<thead>
<tr>
<th scope="col">{ "#" }</th>
<th scope="col">{ "Header" }</th>
<th scope="col">{ "Header" }</th>
<th scope="col">{ "Header" }</th>
<th scope="col">{ "Header" }</th>
</tr>
</thead>
<tbody>
<tr>
<td class={CSS_CELL}>{ "1,001" }</td>
<td class={CSS_CELL}>{ "random" }</td>
<td class={CSS_CELL}>{ "data" }</td>
<td class={CSS_CELL}>{ "placeholder" }</td>
<td class={CSS_CELL}>{ "text" }</td>
</tr>
<tr>
<td class={CSS_CELL}>{ "1,015" }</td>
<td class={CSS_CELL}>{ "random" }</td>
<td class={CSS_CELL}>{ "tabular" }</td>
<td class={CSS_CELL}>{ "informaasdftion" }</td>
<td class={CSS_CELL}>{ "text" }</td>
</tr>
</tbody>
</table>
<div class="vert-center">
<div class="spinner-border text-dark" role="status">
<span class="visually-hidden">{ "Loading..." }</span>
</div>
</div>
</>
}
}
fn view_ready(&self, _ctx: &Context<Self>, apps: &[AppLink]) -> Html {
// Please help me, I don't know how to make a grid look nice 🥺
html! {
<>
<div class={CSS_PAGE_HEADER}>
<h2>{ "Applications list" }</h2>
</div>
{ alpha_warning_banner() }
if !apps.is_empty() {
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
{
apps.iter().map(|applink| {
match &applink {
AppLink::Oauth2 {
display_name, redirect_url
} => {
let redirect_url = redirect_url.to_string();
html!{
<div class="col">
<div class={CSS_CARD}>
<h3>
<a href={ redirect_url.clone() } class={CSS_LINK_DARK_STRETCHED}>{ display_name }</a>
</h3>
<div class={CSS_CARD_BODY}>{ "We could put some text here but that's only if there was an app description!"}
</div>
// <a href={ redirect_url.clone() }><button href={ redirect_url } class="btn btn-secondary" aria-label="Go to App">
// {"Go to App"}
// </button></a>
</div>
</div>
}
}
}
}).collect::<Html>()
}
</div>
}
</>
}
}
fn view_error(&self, _ctx: &Context<Self>, msg: &str, kopid: Option<&str>) -> Html {
html! {
<>
<p class="text-center">
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
</p>
<div class="alert alert-danger" role="alert">
<h2>{ "An Error Occured 🥺" }</h2>
<p>{ msg.to_string() }</p>
<p>
{
if let Some(opid) = kopid.as_ref() {
format!("Operation ID: {}", opid)
} else {
"Local Error".to_string()
}
}
</p>
</div>
<p class="text-center">
<a href="/"><button href="/" class="btn btn-secondary" aria-label="Return home">{"Return to the home page"}</button></a>
</p>
</>
}
}
async fn fetch_user_apps() -> Result<Msg, FetchError> {
// WILLIAM TODO - Add an api end point to get these applinks from
// kanidm based on what you can access.
/*
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::SameOrigin);
opts.credentials(RequestCredentials::SameOrigin);
let request = Request::new_with_str_and_init("/no/such/route", &opts)?;
request
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set header");
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
let status = resp.status();
if status == 200 {
let jsval = JsFuture::from(resp.json()?).await?;
let apps: Vec<()> = serde_wasm_bindgen::from_value(jsval)
.expect_throw("Invalid response type - auth_init::AuthResponse");
Ok(Msg::Ready { apps })
} else {
let headers = resp.headers();
let kopid = headers.get("x-kanidm-opid").ok().flatten();
let text = JsFuture::from(resp.text()?).await?;
let emsg = text.as_string().unwrap_or_default();
Ok(Msg::Error { emsg, kopid })
}
*/
Ok(Msg::Ready {
apps: vec![AppLink::Oauth2 {
display_name: "Test Application".to_string(),
redirect_url: url::Url::parse("http://localhost:8080")
.expect_throw("Failed to setup url"),
}],
})
}
}

View file

@ -1,5 +1,5 @@
use gloo::console;
use kanidm_proto::v1::{UserAuthToken, UiHint};
use kanidm_proto::v1::{UiHint, UserAuthToken};
use serde::{Deserialize, Serialize};
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
@ -7,7 +7,7 @@ use web_sys::{Request, RequestCredentials, RequestInit, RequestMode, Response};
use yew::prelude::*;
use yew_router::prelude::*;
use crate::components::{admin_accounts, admin_groups, admin_oauth2, adminmenu};
use crate::components::{admin_accounts, admin_groups, admin_menu, admin_oauth2};
use crate::error::*;
use crate::manager::Route;
use crate::{models, utils};
@ -444,7 +444,7 @@ impl ViewsApp {
fn admin_routes(route: &AdminRoute) -> Html {
match route {
AdminRoute::AdminMenu => html! {
<adminmenu::AdminMenu />
<admin_menu::AdminMenu />
},
AdminRoute::AdminListAccounts => html!(
<admin_accounts::AdminListAccounts />