Skip to content

Commit 41f5de1

Browse files
authored
feat: Add support for Hive group ownership (#40)
* style: Remove unused import in slide_group_options.rs Removes unused import of `leptos::leptos_dom::logging::console_log` that I accidentally left behind previously. * chore: Add more test users to dev environment Adds 3 new users to the development docker compose environment: - Diana Datalog - Martin Median - Karin Klubbare Also adds the group METAdorerna to that environment, and adds all users to that group. This will be used when developing the auth features. * chore(backend): Add reqwest dependency Adds the reqwest package as a dependency to the backend crate. This will be used for the Hive client. * chore: Add missing libs to shell.nix Adds pkg-config and openssl to the Nix shell configuration. Not sure why it wasn't a problem before, but I'm not able to run `cargo check` without it. Also added some environment variables which I'm like 80 % are necessary. * fix(dev): Make `app` depend on `nyckeln` Marks the `nyckeln` service as a dependency for the app service in the Docker compose config. This makes `app` wait until `nyckeln` has started before continuing. This seems to improve the issue I've personally been having with the OIDC Manager failing to initialize when launching the containers the first time after a code change. The issue was most likely some sort of race condition (but it's very strange that it would start successfully after restarting the containers). This required adding a health checker function which is ran periodically to determine if `nyckeln` is running. The test function isn't perfect as it only checks that the service is listening on it's ports. I decided to not have it try any actual endpoints, as that would result in the requests being logged every 2 seconds. * feat(backend): Add endpoint to get Hive memberships Adds an API endpoint to get the currently logged in user's Hive memberships. This required adding a new Hive client abstraction, as well as adding support for configuring the Hive API token. Note: I have not tested the changes to the production compose file (I wouldn't know how to). I've tried to copy the pattern from the OIDC configuration. * feat: Add support for Hive group ownership Adds support for storing slide group ownership as a Hive group in the backend. This also required changing the frontend so that it can display group ownership. * feat(frontend): Support transfering ownership Adds a new transfer ownership button to the slide group options view.
1 parent 5bfd73d commit 41f5de1

22 files changed

Lines changed: 693 additions & 69 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
CLIENT_ID=
22
CLIENT_SECRET=
3+
HIVE_SECRET=

Cargo.lock

Lines changed: 71 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

compose.prod.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ services:
77
environment:
88
- ROCKET_DATABASES={sea_orm={url="postgresql://postgres:postgres@db/metatv"}}
99
- ROCKET_OIDC={issuer_url="https://sso.datasektionen.se/op",client_id="${CLIENT_ID}",client_secret="${CLIENT_SECRET}",redirect_url="http://localhost:8000/auth/oidc-callback"}
10+
- ROCKET_HIVE={url="https://hive.datasektionen.se/api/v1",secret="${HIVE_SECRET}"}
1011
- ROCKET_UPLOAD_DIR="./uploads"
1112
- ROCKET_SECRET_KEY=2hMUcPB1UjTd2W8aZDBxoUC27R0z9c56992m3x2MTE4D
1213
- ROCKET_ADDRESS=0.0.0.0

compose.yml

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ services:
77
environment:
88
- ROCKET_DATABASES={sea_orm={url="postgresql://postgres:postgres@db/metatv"}}
99
- ROCKET_OIDC={issuer_url="http://localhost:7003",client_id="client-id",client_secret="client-secret",redirect_url="http://localhost:8000/auth/oidc-callback"}
10+
- ROCKET_HIVE={url="http://nyckeln:7004/api/v1",secret="secret"}
1011
- ROCKET_UPLOAD_DIR="./uploads"
1112
- ROCKET_SECRET_KEY=2hMUcPB1UjTd2W8aZDBxoUC27R0z9c56992m3x2MTE4D
1213
- ROCKET_ADDRESS=0.0.0.0
@@ -21,6 +22,8 @@ services:
2122
depends_on:
2223
db:
2324
condition: service_healthy
25+
nyckeln:
26+
condition: service_healthy
2427
develop:
2528
watch:
2629
- action: rebuild
@@ -48,6 +51,12 @@ services:
4851
configs:
4952
- source: nyckeln.yaml
5053
target: /config.yaml
54+
healthcheck:
55+
test: ["CMD-SHELL", "nc -z localhost 7001 && nc -z localhost 7002 && nc -z localhost 7003 && nc -z localhost 7004"]
56+
interval: 2s
57+
timeout: 2s
58+
retries: 30
59+
start_period: 5s
5160
ports:
5261
- 7001:7001
5362
- 7002:7002
@@ -70,6 +79,13 @@ configs:
7079
proxy_pass http://nyckeln:7003;
7180
}
7281
}
82+
server {
83+
listen 7004;
84+
85+
location / {
86+
proxy_pass http://nyckeln:7004;
87+
}
88+
}
7389
}
7490
7591
nyckeln.yaml:
@@ -86,9 +102,21 @@ configs:
86102
email: turetek@kth.se
87103
first_name: Ture
88104
family_name: Teknolog
89-
hive_tags:
90-
- id: personal_email
91-
content: turetek@gmail.com
105+
- ug_kth_id: some-other-id
106+
kth_id: diadat
107+
email: diadat@kth.se
108+
first_name: Diana
109+
family_name: Datalog
110+
- ug_kth_id: yet-another-id
111+
kth_id: marmed
112+
email: marmed@kth.se
113+
first_name: Martin
114+
family_name: Median
115+
- ug_kth_id: even-more
116+
kth_id: karklu
117+
email: karklu@kth.se
118+
first_name: Karin
119+
family_name: Klubbare
92120
93121
hive:
94122
groups:
@@ -100,3 +128,14 @@ configs:
100128
permissions:
101129
- id: admin
102130
scope: null
131+
tags:
132+
- id: slide-manager
133+
- name: METAdorerna
134+
id: metadorerna
135+
domain: example.com
136+
members:
137+
- diadat
138+
- marmed
139+
tags:
140+
- id: slide-manager
141+

crates/backend/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ entity = { path = "../entity" }
1313
entity-tag = "0.1.8"
1414
migration = { path = "../migration" }
1515
openidconnect = { version = "4.0.0", features = ["timing-resistant-secret-traits"] }
16+
reqwest = { version = "0.12.24", default-features = false, features = ["charset", "rustls-tls", "http2", "system-proxy", "json"] }
1617
rocket = { version = "0.5.1", features = ["json", "secrets"] }
1718
sea-orm = { version = "1.1.4", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] }
1819
sea-orm-rocket = "0.5.5"

crates/backend/src/auth/hive.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use common::dtos::{LangDto, TaggedGroupDto};
2+
use reqwest::Url;
3+
use rocket::{
4+
fairing::{self, Fairing},
5+
Build, Rocket,
6+
};
7+
use serde::{Deserialize, Serialize};
8+
9+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10+
pub struct HiveConfig {
11+
pub url: Url,
12+
pub secret: String,
13+
}
14+
15+
pub struct HiveClient {
16+
client: reqwest::Client,
17+
config: HiveConfig,
18+
}
19+
20+
impl HiveClient {
21+
pub fn new(config: HiveConfig) -> reqwest::Result<Self> {
22+
let client = reqwest::Client::builder().build()?;
23+
24+
Ok(Self { client, config })
25+
}
26+
27+
/// Get group memberships for the user with the specified username that have been tagged with
28+
/// META TV's tag.
29+
///
30+
/// On a high level this can be thought of as returning the group memberships for the user that
31+
/// this application is allowed to see.
32+
///
33+
/// Wrapper around `/tagged/slide-manager/memberships/{username}`.
34+
pub async fn tagged_memberships(
35+
&self,
36+
username: &str,
37+
lang: LangDto,
38+
) -> reqwest::Result<Vec<TaggedGroupDto>> {
39+
let mut url = self.config.url.clone();
40+
url.path_segments_mut()
41+
.expect("url can be a base")
42+
.pop_if_empty()
43+
.extend(&["tagged", "slide-manager", "memberships", username]);
44+
url.query_pairs_mut()
45+
.append_pair("lang", &format!("{}", lang));
46+
self.client
47+
.get(url)
48+
.bearer_auth(&self.config.secret)
49+
.send()
50+
.await?
51+
.error_for_status()?
52+
.json::<Vec<TaggedGroupDto>>()
53+
.await
54+
}
55+
56+
/// Get all groups that have been tagged with META TV's tag.
57+
///
58+
/// Wrapper around `/tagged/slide-manager/groups`.
59+
pub async fn tagged_groups(&self, lang: LangDto) -> reqwest::Result<Vec<TaggedGroupDto>> {
60+
let mut url = self.config.url.clone();
61+
url.path_segments_mut()
62+
.expect("url can be a base")
63+
.pop_if_empty()
64+
.extend(&["tagged", "slide-manager", "groups"]);
65+
url.query_pairs_mut()
66+
.append_pair("lang", &format!("{}", lang));
67+
self.client
68+
.get(url)
69+
.bearer_auth(&self.config.secret)
70+
.send()
71+
.await?
72+
.error_for_status()?
73+
.json::<Vec<TaggedGroupDto>>()
74+
.await
75+
}
76+
}
77+
78+
pub struct HiveInitializer;
79+
80+
#[rocket::async_trait]
81+
impl Fairing for HiveInitializer {
82+
fn info(&self) -> fairing::Info {
83+
fairing::Info {
84+
name: "Hive Client",
85+
kind: fairing::Kind::Ignite,
86+
}
87+
}
88+
89+
async fn on_ignite(&self, rocket: Rocket<Build>) -> fairing::Result {
90+
let config = match rocket.figment().focus("hive").extract::<HiveConfig>() {
91+
Ok(config) => config,
92+
Err(e) => {
93+
error!("hive configuration incomplete: {}", e);
94+
return Err(rocket);
95+
}
96+
};
97+
98+
let client = match HiveClient::new(config) {
99+
Ok(client) => client,
100+
Err(e) => {
101+
error!("failed to initialize hive client: {}", e);
102+
return Err(rocket);
103+
}
104+
};
105+
106+
Ok(rocket.manage(client))
107+
}
108+
}

crates/backend/src/auth/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize};
99

1010
use crate::error::AppError;
1111

12+
pub mod hive;
1213
pub mod oidc;
1314

1415
// can't be __Host- because it would not work on http://localhost in Chrome

crates/backend/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ pub enum AppError {
4343
StateSerializationError(#[source] serde_json::Error),
4444
#[error("failed to deserialize internal state from secure storage: {0}")]
4545
StateDeserializationError(#[source] serde_json::Error), // not from client-controlled
46+
#[error("failed to complete internal request: {0}")]
47+
InternalRequestFailure(#[from] reqwest::Error),
4648
#[error("authentication flow expired and can no longer be completed")]
4749
AuthenticationFlowExpired,
4850
}
@@ -64,6 +66,7 @@ impl AppError {
6466
AppError::OidcAuthenticationError(_) => Status::InternalServerError,
6567
AppError::StateSerializationError(_) => Status::InternalServerError,
6668
AppError::StateDeserializationError(_) => Status::InternalServerError,
69+
AppError::InternalRequestFailure(_) => Status::InternalServerError,
6770
AppError::AuthenticationFlowExpired => Status::Gone,
6871
}
6972
}

0 commit comments

Comments
 (0)