diff --git a/docker-compose.yml b/docker-compose.yml index 91190c0..a46dd20 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -66,3 +66,11 @@ services: - "./tmp/espocrm:/var/www/html" depends_on: - mariadb-espocrm + + nginx: + image: nginx + network_mode: "host" + volumes: + - "./localhost.pem:/etc/ssl/certs/ssl-cert-snakeoil.pem" + - "./localhost-key.pem:/etc/ssl/private/ssl-cert-snakeoil.key" + - "./nginx.conf:/etc/nginx/conf.d/default.conf:ro" \ No newline at end of file diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 28f0e18..b79c4d5 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -1,5 +1,8 @@ # Summary - [Introduction](introduction.md) +- [Deploying](./deploy/index.md) + - [Configuration](./deploy/configuration.md) + - [OAuth2 Proxy](./deploy/oauth2_proxy.md) - [OAuth2](oauth2/index.md) - [Authorization](oauth2/authorization.md) - [API](api/index.md) diff --git a/docs/src/deploy/configuration.md b/docs/src/deploy/configuration.md new file mode 100644 index 0000000..8980f78 --- /dev/null +++ b/docs/src/deploy/configuration.md @@ -0,0 +1,19 @@ +# Configuration + + +## Default config file +```json +{{#include ../../../sample_config.json}} +``` + +## Environmental variables +``` +CONFIG_PATH= +``` + +## Available options +The following Rust structs define the layout of the configuration. +An example of how this translates to JSON can be found in the [sample config](#default-config-file) +```rust,noplayground +{{#include ../../../server/wilford/src/config.rs:config}} +``` \ No newline at end of file diff --git a/docs/src/deploy/index.md b/docs/src/deploy/index.md new file mode 100644 index 0000000..5ccaf93 --- /dev/null +++ b/docs/src/deploy/index.md @@ -0,0 +1 @@ +# Deployment Documentation \ No newline at end of file diff --git a/docs/src/deploy/oauth2_proxy.md b/docs/src/deploy/oauth2_proxy.md new file mode 100644 index 0000000..b26b1c7 --- /dev/null +++ b/docs/src/deploy/oauth2_proxy.md @@ -0,0 +1,96 @@ +# OAuth2 Proxy + +Wilford supports running with [oauth2-proxy](https://oauth2-proxy.github.io/oauth2-proxy/). +Using oaut2-proxy together with Wilford and nginx, you can protect static resources without needing to modify them. + +## OAuth2 Config +Sample docker-compose file for running oauth2-proxy. +Replace the `CLIENT_ID` and `CLIENT_SECRET` with the ID and Secret generated by Wilford. Use `REDIRECT_URL` as the Redirect URL in Wilford. +The `COOKIE_SECRET` should be a securely generated random string. +```yml +version: '3.2' +services: + oauth2_proxy: + image: quay.io/oauth2-proxy/oauth2-proxy + environment: + - "OAUTH2_PROXY_COOKIE_SECRET=VsZqXqHQzwdPUcEUDgNxmQvTRZ46DtlQr8q-HtomkL8=" + - "OAUTH2_PROXY_COOKIE_SECURE=true" + - "OAUTH2_PROXY_COOKIE_DOMAIN=localhost" + - "OAUTH2_PROXY_CLIENT_ID=NuWrxroZbOuhBL2ufHx9zj0qKT6XXQRg" + - "OAUTH2_PROXY_CLIENT_SECRET=vwn0MqNbD9qAnvCbGns9sNtikWC7eTM2V7DIz85vcimtxm12" + - "OAUTH2_PROXY_OIDC_ISSUER_URL=https://localhost:8443" + - "OAUTH2_PROXY_REDIRECT_URL=https://localhost:8443/oauth2/callback" + - "OAUTH2_PROXY_PROVIDER=oidc" + - "OAUTH2_PROXY_EMAIL_DOMAINS=*" + - "OAUTH2_PROXY_OIDC_EMAIL_CLAIM=sub_email" + - "OAUTH2_PROXY_PROVIDER_DISPLAY_NAME=Koala" + - "OAUTH2_PROXY_CUSTOM_SIGN_IN_LOGO=-" + - "OAUTH2_PROXY_BANNER=" + - "OAUTH2_PROXY_FOOTER=-" + network_mode: "host" +``` + +## Nginx auth_directive +Using this setup, you can use the nginx auth directive very easily: +```conf + location /secure { + auth_request /oauth2/auth; + error_page 401 =403 /oauth2/sign_in; + proxy_pass http://my-secure-backend; + } +``` + +## Localhost +The JWKS specification requires it be served over https. When working locally, this can be a bit of a pain. + +### Generate certificates for localhost +Install the required tools: +```bash +sudo apt install -y libnss3-tools mkcert +``` +Generate a CA cert: +```bash +mkcert --install +``` + +Generate SSL certificate for `localhost`, from the repository root: +```bash +mkcert localhost +``` + +### Oauth2-proxy +Add the following to the docker-compose file: +```yml +volumes: + - "/usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro" + - "/etc/ssl/certs:/etc/ssl/certs:ro" +``` + +### nginx +Use the following block for Wilford: +```conf +server { + listen 8443 ssl default_server; + server_name _; + + ssl_certificate /etc/ssl/certs/ssl-cert-localhost.pem; + ssl_certificate_key /etc/ssl/private/ssl-cert-localhost.key; + + location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://localhost:2521; + } +} +``` +docker-compose: +```yml + nginx: + image: nginx + network_mode: "host" + volumes: + - "./localhost.pem:/etc/ssl/certs/ssl-cert-localhost.pem" + - "./localhost-key.pem:/etc/ssl/private/ssl-cert-localhost.key" + - "./nginx.conf:/etc/nginx/conf.d/default.conf:ro" +``` \ No newline at end of file diff --git a/localhost-key.pem b/localhost-key.pem new file mode 100644 index 0000000..e616212 --- /dev/null +++ b/localhost-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDsNbwWcAH2Dss+ +GPSLZkO1gYb8SOFTbjSw/pXCVudZNiLbCPNWrALSLpgwLiXJQyQLwG40lL4jOOl1 ++p4nKGYl/6zRpPLolvngQH+GVbUJbODiYLY/MqjVdMT3PX97qIKGpqb2zzcPHm2E +ntlTfh7RIHzlvej9zPpccG62a4w9C67oj7Bh+E274HQY96pPSw3eFcictXekPoxS +lIdVnOh+3uP4TI/MnKDrcn346FJqnQZD9AnwQ5B7x4oB+d9BrmMyzWyQqF1jNxbV +CTzoTeKoyqsdJzCuecLeCHN+soSuK2aM9d2Wx5Pb16VUYD8XKMjEYg72tYNrGx7a +whMQ189rAgMBAAECggEAWUmSmJSsWRuMfiOmxM7aR1D3+oN+ETB2YHVLnNOGzfUl +xdAjU57fzh1oz8WR6PslNAAAaIXVPbE0prEeeUTPIAv+gpysaXkwaTFYQypArZhn +hYrzOP5oTY+/KIopl0/CTy3NrTv03xUsZtY45lOlSH3UWG+qE84Y0Tp6zx/mOehn +qVyWC+klhbcYbJDh2bRcddJbYv7TbtHMLKpvIiM2As5mYeDJChDX1oBPRirabALs +gxfvY+NWw/DmL+Nn7QRL6Il6Mr5U5GGLYiRyRlnQHRGPaZ2OjNiRmmxsVYkKcFQ2 +7XWkNl4kKKgA+plqryGKl47blRG0p2fNhADjTax9iQKBgQDt0L9QFEE5YlFWS9/o +wOg2DKKTkDUUnBBKlw0nbx58ygLb0VZaPCoWp/Mukxd7Lry6PBdSMXsIPIZnY+BP +52A3o1a0sy/ssmNL97CJmTyQbDNKoXWzY8W/Ra0m0R6nbnV/CoFhbHmQoZnB4TeP +41t/EIC+1u8CBUIoZOpo4Y+EhwKBgQD+RY8tAvqYpR0XaxgKhxPxSgbtXAH/6dvd +fVVhsUqtfdE4gFZbJmHXdC2vf1bKyHQ7D/g4rqpUe2aauDUSD57WObBfl1w7i/uR +3dAyUFEwme4KsJz5nS0fNQxZ/6+0MFEVtywe3w96Jr15RhltwKZSZxhZehxcnlvH +jpvESDr6/QKBgEFU23nQVqrBC789UOHMPP68Md1//FURGpijLoXqzOFTTb29oI9h +f96BfRkKZ6T7jfVLlMyLs1Tr67Bzi6fn1FL0mFlD8KKBzy2LegATDMRQNTcHbCJA +Ao8tQQgs4tL0UWr5I9nzxuGow2izymPI/dXGXtgOi9JuR2J5drwhWx/5AoGABdPW +SjPNRn5SQl0j+enKnTcTHZGEQjc74MGkmU6U5ZECoIbgc8pXZ7az7Ve/x3n8n/Xn +vHTUVodVfKpIHRfajhJYZnhzlrHInDk3MlAA7Fo6yGfv0RC3HgX7OHzRrBGHajX+ +ft6h3izRHtxqbMeDiFPwjOxthfnjJJmyHDeDkokCgYEA68cGwARZoX1lyzcXGbfb +TwOxJc79TU+zhdQVH/aTI6hzMeTdoc2qT+yPU8qOgJkVoCBM/TaRrIhrpGy+Tf1o +P4Gw1qcJWNMRdg1ssdyKCueCIx+M7DdNPA578UhD5RgdiCUmu0ufLWdC5ZPVxv8c +fFo8K4Bc9wBn4W3UBsnBROc= +-----END PRIVATE KEY----- diff --git a/localhost.pem b/localhost.pem new file mode 100644 index 0000000..927aec2 --- /dev/null +++ b/localhost.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEUjCCArqgAwIBAgIQAKAUPbumOzgi7b+DtrstqDANBgkqhkiG9w0BAQsFADCB +jTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTEwLwYDVQQLDCh0b2Jp +YXNAdG9iaWFzLWRlc2t0b3AgKFRvYmlhcyBkZSBCcnVpam4pMTgwNgYDVQQDDC9t +a2NlcnQgdG9iaWFzQHRvYmlhcy1kZXNrdG9wIChUb2JpYXMgZGUgQnJ1aWpuKTAe +Fw0yNDA2MzAxMzQ3MzBaFw0yNjA5MzAxMzQ3MzBaMFwxJzAlBgNVBAoTHm1rY2Vy +dCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTExMC8GA1UECwwodG9iaWFzQHRvYmlh +cy1kZXNrdG9wIChUb2JpYXMgZGUgQnJ1aWpuKTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAOw1vBZwAfYOyz4Y9ItmQ7WBhvxI4VNuNLD+lcJW51k2ItsI +81asAtIumDAuJclDJAvAbjSUviM46XX6nicoZiX/rNGk8uiW+eBAf4ZVtQls4OJg +tj8yqNV0xPc9f3uogoampvbPNw8ebYSe2VN+HtEgfOW96P3M+lxwbrZrjD0LruiP +sGH4TbvgdBj3qk9LDd4VyJy1d6Q+jFKUh1Wc6H7e4/hMj8ycoOtyffjoUmqdBkP0 +CfBDkHvHigH530GuYzLNbJCoXWM3FtUJPOhN4qjKqx0nMK55wt4Ic36yhK4rZoz1 +3ZbHk9vXpVRgPxcoyMRiDva1g2sbHtrCExDXz2sCAwEAAaNeMFwwDgYDVR0PAQH/ +BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFNFs6uYdWwV2 +bmxT5oolJDjqSqCcMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsF +AAOCAYEAKMi6fznFFhcxHKl94lIW8WBuw8SwbtRCRmugl1UFjT9zHYijLdwSTcPs +1FTIBSskK8xZFWI4b+UQJDe6jZEhuYUHRownx9OYznPy2daBK2Mnh3u7Ni07d7R5 +EomqHurTxVTxC0+hnV487zLpXDZr3Xz0Q9YORuHOYj2ayMUOrahp6aR2ppBWFTAB +bOgioBj9qcavx0YWC5P3WzljG/+G1x2KQQ5Q1zrxy2E4bxwAWpxXjIMvTWjDq/NQ +wD6NCAPbRVvBEM1rgBJ3chKAQqp5VQ3oebxPXyQDoUG/WCTVgBfmO9t6mUT7eONT +pbKRxSP6CGnIdhPFCscODs9CffcfNBznUFj886q2+3vwOnFa1uZCLdQ2i35wurxl +PffKLJS+oJyUpP9qXPY7BNzmodw6hQwLhk4Mv3IGUWJcgfuHkIlsLYkUv41G4BMJ +wHGKflYVCTU+GVeT/M4zP/W5yLM0k1h56s+qgd5d8vlXIvIBnEmV9Y9jcgCT8+Wz +kYkapOB2 +-----END CERTIFICATE----- diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..063689d --- /dev/null +++ b/nginx.conf @@ -0,0 +1,39 @@ +server { + listen 8443 ssl default_server; + server_name _; + + ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem; + ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; + + location /oauth2/ { + proxy_pass http://127.0.0.1:4180; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Auth-Request-Redirect $request_uri; + # or, if you are handling multiple domains: + # proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri; + } + location = /oauth2/auth { + proxy_pass http://127.0.0.1:4180; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Uri $request_uri; + # nginx auth_request includes headers but not body + proxy_set_header Content-Length ""; + proxy_pass_request_body off; + } + + location /docs { + auth_request /oauth2/auth; + error_page 401 =403 /oauth2/sign_in; + proxy_pass https://google.com; + } + + + location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://localhost:2521; + } +} \ No newline at end of file diff --git a/oauth2_proxy.docker-compose.yml b/oauth2_proxy.docker-compose.yml index cde990e..148b0ed 100644 --- a/oauth2_proxy.docker-compose.yml +++ b/oauth2_proxy.docker-compose.yml @@ -2,13 +2,22 @@ version: '3.2' services: oauth2_proxy: image: quay.io/oauth2-proxy/oauth2-proxy + volumes: + - "/usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro" + - "/etc/ssl/certs:/etc/ssl/certs:ro" environment: - "OAUTH2_PROXY_COOKIE_SECRET=VsZqXqHQzwdPUcEUDgNxmQvTRZ46DtlQr8q-HtomkL8=" - - "OAUTH2_PROXY_CLIENT_ID=Vy1l9P2X7RFf4jHfo3OiLHtShllSZKDa" - - "OAUTH2_PROXY_CLIENT_SECRET=d4HtCZLeZvRvldGOxQd18l5a46hZEl6WWoocL2IbkfcozLH6" - - "OAUTH2_PROXY_OIDC_ISSUER_URL=http://localhost:2521" - - "OAUTH2_PROXY_REDIRECT_URL=http://127.0.0.1:4180/oauth2/callback" + - "OAUTH2_PROXY_COOKIE_SECURE=true" + - "OAUTH2_PROXY_COOKIE_DOMAIN=localhost" + - "OAUTH2_PROXY_CLIENT_ID=NuWrxroZbOuhBL2ufHx9zj0qKT6XXQRg" + - "OAUTH2_PROXY_CLIENT_SECRET=vwn0MqNbD9qAnvCbGns9sNtikWC7eTM2V7DIz85vcimtxm12" + - "OAUTH2_PROXY_OIDC_ISSUER_URL=https://localhost:8443" + - "OAUTH2_PROXY_REDIRECT_URL=https://localhost:8443/oauth2/callback" - "OAUTH2_PROXY_PROVIDER=oidc" - "OAUTH2_PROXY_EMAIL_DOMAINS=*" - - "OAUTH2_PROXY_OIDC_JWKS_URL=http://localhost:2521/.well-known/jwks.json" - network_mode: "host" \ No newline at end of file + - "OAUTH2_PROXY_OIDC_EMAIL_CLAIM=sub_email" + - "OAUTH2_PROXY_PROVIDER_DISPLAY_NAME=Koala" + - "OAUTH2_PROXY_CUSTOM_SIGN_IN_LOGO=-" + - "OAUTH2_PROXY_BANNER=" + - "OAUTH2_PROXY_FOOTER=-" + network_mode: "host" diff --git a/server/Cargo.lock b/server/Cargo.lock index 2ce01e9..e743239 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -2935,6 +2935,7 @@ dependencies = [ "noiseless-tracing-actix-web", "pem", "reqwest", + "rsa", "serde", "serde_json", "serde_qs", @@ -2943,6 +2944,7 @@ dependencies = [ "tokio", "tracing", "tracing-actix-web", + "tracing-error", "tracing-subscriber", ] diff --git a/server/database/migrations/1_initial.sql b/server/database/migrations/1_initial.sql index a889ce6..d154051 100644 --- a/server/database/migrations/1_initial.sql +++ b/server/database/migrations/1_initial.sql @@ -12,7 +12,7 @@ CREATE TABLE oauth2_pending_authorizations ( client_id VARCHAR(32) NOT NULL, scopes TEXT DEFAULT NULL, state TEXT DEFAULT NULL, - espo_user_id TEXT DEFAULT NULL, + user_id TEXT DEFAULT NULL, ty TEXT NOT NULL, nonce TEXT DEFAULT NULL, PRIMARY KEY (id) @@ -23,7 +23,7 @@ CREATE TABLE oauth2_access_tokens ( client_id VARCHAR(32) NOT NULL, expires_at BIGINT NOT NULL, issued_at BIGINT NOT NULL, - espo_user_id VARCHAR(64) NOT NULL, + user_id VARCHAR(64) NOT NULL, scopes TEXT DEFAULT NULL, PRIMARY KEY (token) ); @@ -31,7 +31,7 @@ CREATE TABLE oauth2_access_tokens ( CREATE TABLE oauth2_refresh_tokens ( token VARCHAR(32) NOT NULL, client_id VARCHAR(32) NOT NULL, - espo_user_id VARCHAR(64) NOT NULL, + user_id VARCHAR(64) NOT NULL, scopes TEXT DEFAULT NULL, PRIMARY KEY (token) ); @@ -41,22 +41,23 @@ CREATE TABLE oauth2_authorization_codes ( code VARCHAR(32) NOT NULL, expires_at BIGINT NOT NULL, scopes TEXT DEFAULT NULL, - espo_user_id TEXT NOT NULL, + user_id TEXT NOT NULL, nonce TEXT DEFAULT NULL, PRIMARY KEY (code) ); CREATE TABLE users ( - espo_user_id VARCHAR(64) NOT NULL, + user_id VARCHAR(64) NOT NULL, name TEXT NOT NULL, - is_espo_admin BOOL, - PRIMARY KEY (espo_user_id) + email TEXT NOT NULL, + is_admin BOOL, + PRIMARY KEY (user_id) ); CREATE TABLE user_permitted_scopes ( - espo_user_id VARCHAR(64) NOT NULL, + user_id VARCHAR(64) NOT NULL, scope VARCHAR(64) NOT NULL, - PRIMARY KEY (espo_user_id, scope) + PRIMARY KEY (user_id, scope) ); CREATE TABLE constant_access_tokens ( diff --git a/server/database/src/constant_access_tokens.rs b/server/database/src/constant_access_tokens.rs index f7d3420..251d469 100644 --- a/server/database/src/constant_access_tokens.rs +++ b/server/database/src/constant_access_tokens.rs @@ -2,6 +2,7 @@ use crate::driver::Database; use crate::generate_string; use sqlx::FromRow; use sqlx::Result; +use tracing::instrument; #[derive(Debug, Clone, FromRow)] pub struct ConstantAccessToken { @@ -14,6 +15,7 @@ impl ConstantAccessToken { generate_string(32) } + #[instrument] pub async fn new(driver: &Database, name: String) -> Result { let token = Self::generate_token(); @@ -26,12 +28,14 @@ impl ConstantAccessToken { Ok(Self { name, token }) } + #[instrument] pub async fn list(driver: &Database) -> Result> { Ok(sqlx::query_as("SELECT * FROM constant_access_tokens") .fetch_all(&**driver) .await?) } + #[instrument] pub async fn get_by_token(driver: &Database, token: &str) -> Result> { Ok( sqlx::query_as("SELECT * FROM constant_access_tokens WHERE token = ?") @@ -41,6 +45,7 @@ impl ConstantAccessToken { ) } + #[instrument] pub async fn revoke(self, driver: &Database) -> Result<()> { sqlx::query("DELETE FROM constant_access_tokens WHERE token = ?") .bind(self.token) diff --git a/server/database/src/oauth2_client.rs b/server/database/src/oauth2_client.rs index 5551178..14a77bb 100644 --- a/server/database/src/oauth2_client.rs +++ b/server/database/src/oauth2_client.rs @@ -1,4 +1,5 @@ use crate::driver::Database; +use crate::user::User; use crate::{generate_string, impl_enum_type}; use jwt_simple::algorithms::{RS256KeyPair, RSAKeyPairLike}; use jwt_simple::claims::Claims; @@ -7,7 +8,7 @@ use sqlx::{Decode, Encode, FromRow, Result}; use std::collections::HashSet; use thiserror::Error; use time::{Duration, OffsetDateTime}; -use crate::user::User; +use tracing::instrument; #[derive(Debug, Clone, FromRow)] pub struct OAuth2Client { @@ -100,7 +101,7 @@ struct _OAuth2PendingAuthorization { nonce: Option, } -#[derive(FromRow)] +#[derive(Debug, FromRow)] pub struct OAuth2AuthorizationCode { pub code: String, pub client_id: String, @@ -120,7 +121,7 @@ pub struct AccessToken { pub scopes: Option, } -#[derive(FromRow)] +#[derive(Debug, FromRow)] pub struct RefreshToken { pub token: String, pub client_id: String, @@ -186,6 +187,7 @@ impl OAuth2Client { (OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp() } + #[instrument] pub async fn new( driver: &Database, name: String, @@ -213,12 +215,14 @@ impl OAuth2Client { }) } + #[instrument] pub async fn list(driver: &Database) -> Result> { Ok(sqlx::query_as("SELECT * FROM oauth2_clients") .fetch_all(&**driver) .await?) } + #[instrument] pub async fn delete(self, driver: &Database) -> Result<()> { sqlx::query("DELETE FROM oauth2_clients WHERE client_id = ?") .bind(self.client_id) @@ -227,6 +231,7 @@ impl OAuth2Client { Ok(()) } + #[instrument] pub async fn get_by_client_id(driver: &Database, client_id: &str) -> Result> { Ok( sqlx::query_as("SELECT * FROM oauth2_clients WHERE client_id = ?") @@ -236,6 +241,7 @@ impl OAuth2Client { ) } + #[instrument] pub async fn new_pending_authorization( &self, driver: &Database, @@ -267,6 +273,7 @@ impl OAuth2Client { )) } + #[instrument] pub async fn new_authorization_code( &self, driver: &Database, @@ -311,6 +318,7 @@ impl OAuth2Client { }) } + #[instrument] pub async fn new_access_token( &self, driver: &Database, @@ -356,6 +364,7 @@ impl OAuth2Client { }) } + #[instrument] pub async fn new_token_pair( &self, driver: &Database, @@ -414,6 +423,7 @@ impl OAuth2Client { )) } + #[instrument] pub async fn refresh_access_token( &self, driver: &Database, @@ -444,6 +454,7 @@ impl OAuth2Client { } impl AccessToken { + #[instrument] pub async fn get_by_token(driver: &Database, token: &str) -> Result> { Ok( sqlx::query_as("SELECT * FROM oauth2_access_tokens WHERE token = ?") @@ -453,6 +464,7 @@ impl AccessToken { ) } + #[instrument] pub async fn get_with_validation( driver: &Database, token: &str, @@ -473,6 +485,7 @@ impl AccessToken { ) } + #[instrument] pub fn scopes(&self) -> HashSet { self.scopes .as_ref() @@ -482,6 +495,7 @@ impl AccessToken { } impl RefreshToken { + #[instrument] pub async fn get_by_token(driver: &Database, token: &str) -> Result> { Ok( sqlx::query_as("SELECT * FROM oauth2_refresh_tokens WHERE token = ?") @@ -493,6 +507,7 @@ impl RefreshToken { } impl OAuth2PendingAuthorization { + #[instrument] pub async fn get_by_id( driver: &Database, id: &str, @@ -506,6 +521,7 @@ impl OAuth2PendingAuthorization { ) } + #[instrument] pub async fn set_user_id( self, driver: &Database, @@ -525,17 +541,15 @@ impl OAuth2PendingAuthorization { .await?; let new_self = match self { - Self::Unauthorized(v) => { - Self::Authorized(OAuth2PendingAuthorizationAuthorized { - id: v.id, - client_id: v.client_id, - user_id: user_id.to_string(), - state: v.state, - scopes: v.scopes, - ty: v.ty, - nonce: v.nonce, - }) - } + Self::Unauthorized(v) => Self::Authorized(OAuth2PendingAuthorizationAuthorized { + id: v.id, + client_id: v.client_id, + user_id: user_id.to_string(), + state: v.state, + scopes: v.scopes, + ty: v.ty, + nonce: v.nonce, + }), Self::Authorized(_) => unreachable!(), }; @@ -544,6 +558,7 @@ impl OAuth2PendingAuthorization { } impl OAuth2AuthorizationCode { + #[instrument] pub async fn get_by_code(driver: &Database, code: &str) -> Result> { Ok( sqlx::query_as("SELECT * FROM oauth2_authorization_codes WHERE code = ?") @@ -600,12 +615,12 @@ pub struct IdTokenClaims { azp: String, // We also have some custom claims, this is allowed by the JWT spec - sub_email: String, sub_name: String, sub_is_admin: bool, } +#[derive(Debug)] pub enum JwtSigningAlgorithm { RS256, } @@ -618,6 +633,7 @@ pub enum IdTokenCreationError { Signing(String), } +#[instrument] pub fn create_id_token( issuer: String, client: &OAuth2Client, diff --git a/server/database/src/user.rs b/server/database/src/user.rs index c1ec448..f5dc0b7 100644 --- a/server/database/src/user.rs +++ b/server/database/src/user.rs @@ -1,5 +1,6 @@ use crate::driver::Database; use sqlx::{FromRow, Result}; +use tracing::instrument; #[derive(Debug, FromRow)] pub struct User { @@ -10,6 +11,7 @@ pub struct User { } impl User { + #[instrument] pub async fn new( driver: &Database, user_id: String, @@ -33,6 +35,7 @@ impl User { }) } + #[instrument] pub async fn get_by_id(driver: &Database, id: &str) -> Result> { Ok(sqlx::query_as("SELECT * FROM users WHERE user_id = ?") .bind(id) @@ -40,12 +43,14 @@ impl User { .await?) } + #[instrument] pub async fn list(driver: &Database) -> Result> { Ok(sqlx::query_as("SELECT * FROM users") .fetch_all(&**driver) .await?) } + #[instrument] pub async fn list_permitted_scopes(&self, driver: &Database) -> Result> { Ok( sqlx::query_scalar("SELECT scope FROM user_permitted_scopes WHERE user_id = ?") @@ -55,6 +60,7 @@ impl User { ) } + #[instrument] pub async fn remove_permitted_scope(&self, driver: &Database, scope: &str) -> Result<()> { sqlx::query("DELETE FROM user_permitted_scopes WHERE user_id = ? AND scope = ?") .bind(&self.user_id) @@ -65,6 +71,7 @@ impl User { Ok(()) } + #[instrument] pub async fn grant_permitted_scope(&self, driver: &Database, scope: &str) -> Result<()> { sqlx::query("INSERT INTO user_permitted_scopes (user_id, scope) VALUES (?, ?)") .bind(&self.user_id) diff --git a/server/wilford/Cargo.toml b/server/wilford/Cargo.toml index a341e7b..4437fae 100644 --- a/server/wilford/Cargo.toml +++ b/server/wilford/Cargo.toml @@ -24,4 +24,6 @@ tokio = { version = "1.35.1", features = ["full"] } tracing = "0.1.40" tracing-actix-web = "0.7.9" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -pem = "3.0.4" \ No newline at end of file +pem = "3.0.4" +tracing-error = "0.2.0" +rsa = "0.9.6" \ No newline at end of file diff --git a/server/wilford/src/config.rs b/server/wilford/src/config.rs index 9923c4e..c367b6c 100644 --- a/server/wilford/src/config.rs +++ b/server/wilford/src/config.rs @@ -9,6 +9,7 @@ struct EnvConfig { config_path: PathBuf, } +/* ANCHOR: config */ #[derive(Debug, Deserialize)] pub struct Config { pub http: HttpConfig, @@ -47,6 +48,7 @@ pub struct DatabaseConfig { pub struct DefaultClientConfig { pub redirect_uri: String, } +/* ANCHOR_END: config */ impl EnvConfig { fn new() -> Result { diff --git a/server/wilford/src/espo/user.rs b/server/wilford/src/espo/user.rs index 6f28979..0243acf 100644 --- a/server/wilford/src/espo/user.rs +++ b/server/wilford/src/espo/user.rs @@ -2,7 +2,7 @@ use base64::Engine; use espocrm_rs::{EspoApiClient, Method}; use reqwest::{Result, StatusCode}; use serde::Deserialize; -use tracing::warn; +use tracing::{instrument, Instrument, trace, warn, warn_span}; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] @@ -22,15 +22,19 @@ pub enum LoginStatus { } impl EspoUser { + #[instrument(skip(client))] pub async fn get_by_id(client: &EspoApiClient, id: &str) -> Result { Ok(client .request::<(), &str>(Method::Get, &format!("User/{id}"), None, None) + .instrument(warn_span!("user::by_id")) .await? .error_for_status()? .json() + .instrument(warn_span!("user::by_id::json")) .await?) } + #[instrument(skip_all)] pub async fn try_login( host: &str, username: &str, @@ -51,7 +55,9 @@ impl EspoUser { request = request.header("Espo-Authorization-Code", totp); } - let result = request.send().await?; + let result = request.send() + .instrument(warn_span!("try_login::request")) + .await?; match result.status() { StatusCode::OK => { @@ -67,7 +73,10 @@ impl EspoUser { is_active: bool, } - let payload: Response = result.json().await?; + trace!("Deserializing EspoCRM response"); + let payload: Response = result.json() + .instrument(warn_span!("deserialize")) + .await?; if payload.user.is_active { Ok(LoginStatus::Ok(payload.user.id)) } else { @@ -80,7 +89,10 @@ impl EspoUser { message: String, } - let payload: Response = result.json().await?; + trace!("Deserializing EspoCRM response"); + let payload: Response = result.json() + .instrument(warn_span!("deserialize")) + .await?; if payload.message.eq("enterTotpCode") { Ok(LoginStatus::SecondStepRequired) } else { diff --git a/server/wilford/src/main.rs b/server/wilford/src/main.rs index 0648d9c..78649bf 100644 --- a/server/wilford/src/main.rs +++ b/server/wilford/src/main.rs @@ -10,6 +10,7 @@ use espocrm_rs::EspoApiClient; use noiseless_tracing_actix_web::NoiselessRootSpanBuilder; use tracing::info; use tracing_actix_web::TracingLogger; +use tracing_error::ErrorLayer; use tracing_subscriber::fmt::layer; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; @@ -97,7 +98,19 @@ fn install_tracing() { } tracing_subscriber::registry() - .with(EnvFilter::from_default_env()) - .with(layer().compact()) + .with(EnvFilter::from_default_env() + .add_directive("rustls=WARN" + .parse() + .expect("Invalid tracing directive") + ) + .add_directive("rustls=WARN" + .parse() + .expect("Invalid tracing directive") + ) + ) + .with(layer() + .pretty() + ) + .with(ErrorLayer::default()) .init(); } diff --git a/server/wilford/src/routes/auth.rs b/server/wilford/src/routes/auth.rs index 16091e7..0ef385d 100644 --- a/server/wilford/src/routes/auth.rs +++ b/server/wilford/src/routes/auth.rs @@ -1,6 +1,6 @@ use crate::espo::user::EspoUser; use crate::routes::appdata::{WDatabase, WEspo}; -use crate::routes::error::{WebError, WebResult}; +use crate::routes::error::{WebError, WebErrorKind, WebResult}; use actix_web::cookie::time::OffsetDateTime; use actix_web::dev::Payload; use actix_web::{FromRequest, HttpRequest}; @@ -39,17 +39,17 @@ impl FromRequest for Auth { let token_info = match AccessToken::get_by_token(&database, &token).await? { Some(v) => { if v.expires_at < OffsetDateTime::now_utc().unix_timestamp() { - return Err(WebError::Unauthorized); + return Err(WebErrorKind::Unauthorized.into()); } else { v } } - None => return Err(WebError::Unauthorized), + None => return Err(WebErrorKind::Unauthorized.into()), }; let espo_user = EspoUser::get_by_id(&espo_client, &token_info.user_id) .await - .map_err(|e| WebError::Espo(e))?; + .map_err(|e| WebErrorKind::Espo(e))?; Ok(Self { espo_user_id: espo_user.id, @@ -94,7 +94,7 @@ impl FromRequest for ConstantAccessTokenAuth { let token = get_authorization_token(&req)?; let cat = ConstantAccessToken::get_by_token(&database, &token) .await? - .ok_or(WebError::Unauthorized)?; + .ok_or(WebErrorKind::Unauthorized)?; Ok(Self { name: cat.name, @@ -122,5 +122,5 @@ fn get_authorization_token(req: &HttpRequest) -> WebResult { _ => {} } - Err(WebError::Unauthorized) + Err(WebErrorKind::Unauthorized.into()) } diff --git a/server/wilford/src/routes/error.rs b/server/wilford/src/routes/error.rs index 8c2475d..41b2fd7 100644 --- a/server/wilford/src/routes/error.rs +++ b/server/wilford/src/routes/error.rs @@ -1,11 +1,44 @@ +use std::fmt; +use std::fmt::{Formatter, Write}; use actix_web::http::StatusCode; use actix_web::ResponseError; use thiserror::Error; +use tracing_error::SpanTrace; pub type WebResult = Result; +#[derive(Error)] +#[error("{}", kind)] +pub struct WebError { + kind: WebErrorKind, + context: SpanTrace, +} + +impl fmt::Debug for WebError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.kind.fmt(f)?; + f.write_char('\n')?; + f.write_str(&self.context.to_string())?; + f.write_char('\n')?; + Ok(()) + } +} + +impl From for WebError +where + E: Into, +{ + #[track_caller] + fn from(value: E) -> Self { + Self { + kind: value.into(), + context: SpanTrace::capture(), + } + } +} + #[derive(Debug, Error)] -pub enum WebError { +pub enum WebErrorKind { #[error("Not found")] NotFound, #[error("Bad request")] @@ -22,19 +55,22 @@ pub enum WebError { Espo(reqwest::Error), #[error("Internal server error")] InternalServerError, + #[error("Failed to parse PKCS8 SPKI: {0}")] + RsaPkcs8Spki(#[from] rsa::pkcs8::spki::Error), } impl ResponseError for WebError { fn status_code(&self) -> StatusCode { - match self { - Self::NotFound => StatusCode::NOT_FOUND, - Self::BadRequest => StatusCode::BAD_REQUEST, - Self::Unauthorized => StatusCode::UNAUTHORIZED, - Self::Forbidden => StatusCode::FORBIDDEN, - Self::InvalidInternalState => StatusCode::INTERNAL_SERVER_ERROR, - Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::Espo(_) => StatusCode::BAD_GATEWAY, - Self::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, + match self.kind { + WebErrorKind::NotFound => StatusCode::NOT_FOUND, + WebErrorKind::BadRequest => StatusCode::BAD_REQUEST, + WebErrorKind::Unauthorized => StatusCode::UNAUTHORIZED, + WebErrorKind::Forbidden => StatusCode::FORBIDDEN, + WebErrorKind::InvalidInternalState => StatusCode::INTERNAL_SERVER_ERROR, + WebErrorKind::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, + WebErrorKind::Espo(_) => StatusCode::BAD_GATEWAY, + WebErrorKind::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, + WebErrorKind::RsaPkcs8Spki(_) => StatusCode::INTERNAL_SERVER_ERROR, } } } diff --git a/server/wilford/src/routes/oauth/token.rs b/server/wilford/src/routes/oauth/token.rs index 2b5afa6..ebbbb81 100644 --- a/server/wilford/src/routes/oauth/token.rs +++ b/server/wilford/src/routes/oauth/token.rs @@ -7,10 +7,10 @@ use actix_web::web; use database::oauth2_client::{ create_id_token, JwtSigningAlgorithm, OAuth2AuthorizationCode, OAuth2Client, RefreshToken, }; +use database::user::User; use serde::{Deserialize, Serialize}; use tap::TapFallible; use tracing::warn; -use database::user::User; #[derive(Deserialize)] pub struct Form { @@ -93,7 +93,8 @@ pub async fn token( id_token: create_id_token( config.oidc_issuer.clone(), &client, - &User::get_by_id(&database, &rtoken.user_id).await + &User::get_by_id(&database, &rtoken.user_id) + .await .map_err(|_| OAuth2ErrorKind::ServerError)? .ok_or(OAuth2ErrorKind::ServerError)?, &oidc_signing_key.0, @@ -135,7 +136,8 @@ pub async fn token( id_token: create_id_token( config.oidc_issuer.clone(), &client, - &User::get_by_id(&database, &rtoken.user_id).await + &User::get_by_id(&database, &rtoken.user_id) + .await .map_err(|_| OAuth2ErrorKind::ServerError)? .ok_or(OAuth2ErrorKind::ServerError)?, &oidc_signing_key.0, diff --git a/server/wilford/src/routes/v1/auth/authorization_info.rs b/server/wilford/src/routes/v1/auth/authorization_info.rs index a77de1e..b0c21ce 100644 --- a/server/wilford/src/routes/v1/auth/authorization_info.rs +++ b/server/wilford/src/routes/v1/auth/authorization_info.rs @@ -1,9 +1,11 @@ -use crate::routes::appdata::WDatabase; -use crate::routes::error::{WebError, WebResult}; use actix_web::web; -use database::oauth2_client::{OAuth2Client, OAuth2PendingAuthorization}; use serde::{Deserialize, Serialize}; +use database::oauth2_client::{OAuth2Client, OAuth2PendingAuthorization}; + +use crate::routes::appdata::WDatabase; +use crate::routes::error::{WebErrorKind, WebResult}; + #[derive(Deserialize)] pub struct Query { authorization: String, @@ -21,16 +23,18 @@ pub async fn authorization_info( ) -> WebResult> { let authorization = OAuth2PendingAuthorization::get_by_id(&database, &query.authorization) .await? - .ok_or(WebError::NotFound)?; + .ok_or(WebErrorKind::NotFound)?; match &authorization { OAuth2PendingAuthorization::Authorized(_) => {} - OAuth2PendingAuthorization::Unauthorized(_) => return Err(WebError::Unauthorized), + OAuth2PendingAuthorization::Unauthorized(_) => { + return Err(WebErrorKind::Unauthorized.into()) + } } let client = OAuth2Client::get_by_client_id(&database, &authorization.client_id()) .await? - .ok_or(WebError::NotFound)?; + .ok_or(WebErrorKind::NotFound)?; Ok(web::Json(Response { client_name: client.name, diff --git a/server/wilford/src/routes/v1/auth/authorize.rs b/server/wilford/src/routes/v1/auth/authorize.rs index 3fa5fbb..d04f6af 100644 --- a/server/wilford/src/routes/v1/auth/authorize.rs +++ b/server/wilford/src/routes/v1/auth/authorize.rs @@ -13,7 +13,7 @@ use database::user::User; use crate::response_types::Redirect; use crate::routes::appdata::{WConfig, WDatabase}; -use crate::routes::error::{WebError, WebResult}; +use crate::routes::error::{WebErrorKind, WebResult}; use crate::routes::oauth::{OAuth2AuthorizationResponse, OAuth2Error, OAuth2ErrorKind}; use crate::routes::WOidcSigningKey; @@ -33,11 +33,11 @@ pub async fn authorize( let pending_authorization = OAuth2PendingAuthorization::get_by_id(&database, &query.authorization) .await? - .ok_or(WebError::NotFound)?; + .ok_or(WebErrorKind::NotFound)?; let client = OAuth2Client::get_by_client_id(&database, &pending_authorization.client_id()) .await? - .ok_or(WebError::NotFound)?; + .ok_or(WebErrorKind::NotFound)?; if !query.grant { return Ok(OAuth2AuthorizationResponse::Err(OAuth2Error::new( @@ -54,9 +54,9 @@ pub async fn authorize( .new_authorization_code(&database, pending_authorization) .await .map_err(|e| match e { - OAuth2AuthorizationCodeCreationError::Sqlx(e) => WebError::Database(e), + OAuth2AuthorizationCodeCreationError::Sqlx(e) => WebErrorKind::Database(e), OAuth2AuthorizationCodeCreationError::Unauthorized => { - WebError::InvalidInternalState + WebErrorKind::InvalidInternalState } })?; @@ -98,15 +98,16 @@ pub async fn authorize( create_id_token( config.oidc_issuer.clone(), &client, - &User::get_by_id(&database, &access_token.user_id).await? - .ok_or(WebError::InternalServerError)?, + &User::get_by_id(&database, &access_token.user_id) + .await? + .ok_or(WebErrorKind::InternalServerError)?, &oidc_signing_key.0, &access_token, nonce, JwtSigningAlgorithm::RS256, ) .tap_err(|e| warn!("Failed to create ID token: {e}")) - .map_err(|_| WebError::InternalServerError)? + .map_err(|_| WebErrorKind::InternalServerError)? ), access_token, state @@ -129,8 +130,10 @@ async fn new_access_token( .new_access_token(&database, pending_authorization) .await .map_err(|e| match e { - OAuth2AuthorizationCodeCreationError::Sqlx(e) => WebError::Database(e), - OAuth2AuthorizationCodeCreationError::Unauthorized => WebError::InvalidInternalState, + OAuth2AuthorizationCodeCreationError::Sqlx(e) => WebErrorKind::Database(e), + OAuth2AuthorizationCodeCreationError::Unauthorized => { + WebErrorKind::InvalidInternalState + } })?) } diff --git a/server/wilford/src/routes/v1/auth/login.rs b/server/wilford/src/routes/v1/auth/login.rs index f25def6..92d9b24 100644 --- a/server/wilford/src/routes/v1/auth/login.rs +++ b/server/wilford/src/routes/v1/auth/login.rs @@ -1,13 +1,14 @@ use crate::espo::user::{EspoUser, LoginStatus}; use crate::routes::appdata::{WConfig, WDatabase, WEspo}; -use crate::routes::error::{WebError, WebResult}; +use crate::routes::error::{WebErrorKind, WebResult}; use actix_web::web; use database::oauth2_client::OAuth2PendingAuthorization; use database::user::User; use serde::{Deserialize, Serialize}; use std::collections::HashSet; +use tracing::instrument; -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] pub struct Request { authorization: String, username: String, @@ -21,6 +22,7 @@ pub struct Response { totp_required: bool, } +#[instrument(skip_all)] pub async fn login( database: WDatabase, config: WConfig, @@ -29,7 +31,7 @@ pub async fn login( ) -> WebResult> { let authorization = OAuth2PendingAuthorization::get_by_id(&database, &payload.authorization) .await? - .ok_or(WebError::NotFound)?; + .ok_or(WebErrorKind::NotFound)?; let login = EspoUser::try_login( &config.espo.host, @@ -38,7 +40,7 @@ pub async fn login( payload.totp_code.as_deref(), ) .await - .map_err(|e| WebError::Espo(e))?; + .map_err(|e| WebErrorKind::Espo(e))?; // OAuth2 defines `scope` to be all scopes, seperated by a ' ' (space char) // Where duplicates can be ignored. @@ -71,13 +73,13 @@ pub async fn login( .collect::>(); if !disallowed_scopes.is_empty() { - return Err(WebError::Forbidden); + return Err(WebErrorKind::Forbidden.into()); } } None => { let espo_user = EspoUser::get_by_id(&espo, &id) .await - .map_err(|e| WebError::Espo(e))?; + .map_err(|e| WebErrorKind::Espo(e))?; let user = User::new( &database, @@ -96,7 +98,7 @@ pub async fn login( scope_set.difference(&oidc_scopes).collect::>(); if !disallowed_scopes.is_empty() { - return Err(WebError::Forbidden); + return Err(WebErrorKind::Forbidden.into()); } } } @@ -105,7 +107,7 @@ pub async fn login( authorization .set_user_id(&database, &id) .await - .map_err(|_| WebError::BadRequest)?; + .map_err(|_| WebErrorKind::BadRequest)?; Ok(web::Json(Response { status: true, diff --git a/server/wilford/src/routes/v1/cat/add.rs b/server/wilford/src/routes/v1/cat/add.rs index 0f64cff..2b0f30c 100644 --- a/server/wilford/src/routes/v1/cat/add.rs +++ b/server/wilford/src/routes/v1/cat/add.rs @@ -1,11 +1,13 @@ +use actix_web::web; +use serde::Deserialize; + +use database::constant_access_tokens::ConstantAccessToken; + use crate::response_types::Empty; use crate::routes::appdata::WDatabase; use crate::routes::auth::Auth; -use crate::routes::error::{WebError, WebResult}; +use crate::routes::error::{WebErrorKind, WebResult}; use crate::routes::v1::MANAGE_SCOPE; -use actix_web::web; -use database::constant_access_tokens::ConstantAccessToken; -use serde::Deserialize; #[derive(Deserialize)] pub struct Request { @@ -14,7 +16,7 @@ pub struct Request { pub async fn add(database: WDatabase, auth: Auth, payload: web::Json) -> WebResult { if !auth.has_scope(MANAGE_SCOPE) { - return Err(WebError::Forbidden); + return Err(WebErrorKind::Forbidden.into()); } let exists = ConstantAccessToken::list(&database) @@ -24,11 +26,11 @@ pub async fn add(database: WDatabase, auth: Auth, payload: web::Json) - .is_some(); if exists { - return Err(WebError::BadRequest); + return Err(WebErrorKind::BadRequest.into()); } if payload.name.len() > 64 { - return Err(WebError::BadRequest); + return Err(WebErrorKind::BadRequest.into()); } ConstantAccessToken::new(&database, payload.name.clone()).await?; diff --git a/server/wilford/src/routes/v1/cat/list.rs b/server/wilford/src/routes/v1/cat/list.rs index 54f65bf..d637318 100644 --- a/server/wilford/src/routes/v1/cat/list.rs +++ b/server/wilford/src/routes/v1/cat/list.rs @@ -1,10 +1,12 @@ +use actix_web::web; +use serde::Serialize; + +use database::constant_access_tokens::ConstantAccessToken; + use crate::routes::appdata::WDatabase; use crate::routes::auth::Auth; -use crate::routes::error::{WebError, WebResult}; +use crate::routes::error::{WebErrorKind, WebResult}; use crate::routes::v1::MANAGE_SCOPE; -use actix_web::web; -use database::constant_access_tokens::ConstantAccessToken; -use serde::Serialize; #[derive(Serialize)] pub struct Response { @@ -19,7 +21,7 @@ pub struct Cat { pub async fn list(database: WDatabase, auth: Auth) -> WebResult> { if !auth.has_scope(MANAGE_SCOPE) { - return Err(WebError::Forbidden); + return Err(WebErrorKind::Forbidden.into()); } let tokens = ConstantAccessToken::list(&database) diff --git a/server/wilford/src/routes/v1/cat/remove.rs b/server/wilford/src/routes/v1/cat/remove.rs index 65ff491..553f127 100644 --- a/server/wilford/src/routes/v1/cat/remove.rs +++ b/server/wilford/src/routes/v1/cat/remove.rs @@ -1,11 +1,13 @@ +use actix_web::web; +use serde::Deserialize; + +use database::constant_access_tokens::ConstantAccessToken; + use crate::response_types::Empty; use crate::routes::appdata::WDatabase; use crate::routes::auth::Auth; -use crate::routes::error::{WebError, WebResult}; +use crate::routes::error::{WebErrorKind, WebResult}; use crate::routes::v1::MANAGE_SCOPE; -use actix_web::web; -use database::constant_access_tokens::ConstantAccessToken; -use serde::Deserialize; #[derive(Deserialize)] pub struct Request { @@ -18,12 +20,12 @@ pub async fn remove( payload: web::Json, ) -> WebResult { if !auth.has_scope(MANAGE_SCOPE) { - return Err(WebError::Forbidden); + return Err(WebErrorKind::Forbidden.into()); } let cat = ConstantAccessToken::get_by_token(&database, &payload.token) .await? - .ok_or(WebError::NotFound)?; + .ok_or(WebErrorKind::NotFound)?; cat.revoke(&database).await?; Ok(Empty) diff --git a/server/wilford/src/routes/v1/clients/add.rs b/server/wilford/src/routes/v1/clients/add.rs index 6a1648f..1dc5044 100644 --- a/server/wilford/src/routes/v1/clients/add.rs +++ b/server/wilford/src/routes/v1/clients/add.rs @@ -1,11 +1,13 @@ +use actix_web::web; +use serde::Deserialize; + +use database::oauth2_client::OAuth2Client; + use crate::response_types::Empty; use crate::routes::appdata::WDatabase; use crate::routes::auth::Auth; -use crate::routes::error::{WebError, WebResult}; +use crate::routes::error::{WebErrorKind, WebResult}; use crate::routes::v1::MANAGE_SCOPE; -use actix_web::web; -use database::oauth2_client::OAuth2Client; -use serde::Deserialize; #[derive(Deserialize)] pub struct Request { @@ -15,11 +17,11 @@ pub struct Request { pub async fn add(database: WDatabase, auth: Auth, payload: web::Json) -> WebResult { if !auth.has_scope(MANAGE_SCOPE) { - return Err(WebError::Forbidden); + return Err(WebErrorKind::Forbidden.into()); } if payload.name.len() > 64 { - return Err(WebError::BadRequest); + return Err(WebErrorKind::BadRequest.into()); } let exists = OAuth2Client::list(&database) @@ -29,7 +31,7 @@ pub async fn add(database: WDatabase, auth: Auth, payload: web::Json) - .is_some(); if exists { - return Err(WebError::BadRequest); + return Err(WebErrorKind::BadRequest.into()); } OAuth2Client::new( diff --git a/server/wilford/src/routes/v1/clients/internal.rs b/server/wilford/src/routes/v1/clients/internal.rs index 6e9093b..0d21f82 100644 --- a/server/wilford/src/routes/v1/clients/internal.rs +++ b/server/wilford/src/routes/v1/clients/internal.rs @@ -1,9 +1,11 @@ -use crate::routes::appdata::WDatabase; -use crate::routes::error::{WebError, WebResult}; use actix_web::web; -use database::oauth2_client::OAuth2Client; use serde::Serialize; +use database::oauth2_client::OAuth2Client; + +use crate::routes::appdata::WDatabase; +use crate::routes::error::{WebErrorKind, WebResult}; + #[derive(Serialize)] pub struct Response { name: String, @@ -17,7 +19,7 @@ pub async fn internal(database: WDatabase) -> WebResult> { .await? .into_iter() .find(|c| c.is_internal) - .ok_or(WebError::InvalidInternalState)?; + .ok_or(WebErrorKind::InvalidInternalState)?; Ok(web::Json(Response { name: client.name, diff --git a/server/wilford/src/routes/v1/clients/list.rs b/server/wilford/src/routes/v1/clients/list.rs index ce02729..27e3a6a 100644 --- a/server/wilford/src/routes/v1/clients/list.rs +++ b/server/wilford/src/routes/v1/clients/list.rs @@ -1,10 +1,12 @@ +use actix_web::web; +use serde::Serialize; + +use database::oauth2_client::OAuth2Client; + use crate::routes::appdata::WDatabase; use crate::routes::auth::Auth; -use crate::routes::error::{WebError, WebResult}; +use crate::routes::error::{WebErrorKind, WebResult}; use crate::routes::v1::MANAGE_SCOPE; -use actix_web::web; -use database::oauth2_client::OAuth2Client; -use serde::Serialize; #[derive(Serialize)] pub struct Response { @@ -21,7 +23,7 @@ pub struct Client { pub async fn list(database: WDatabase, auth: Auth) -> WebResult> { if !auth.has_scope(MANAGE_SCOPE) { - return Err(WebError::Forbidden); + return Err(WebErrorKind::Forbidden.into()); } let clients = OAuth2Client::list(&database) diff --git a/server/wilford/src/routes/v1/clients/remove.rs b/server/wilford/src/routes/v1/clients/remove.rs index f988756..d31808f 100644 --- a/server/wilford/src/routes/v1/clients/remove.rs +++ b/server/wilford/src/routes/v1/clients/remove.rs @@ -1,11 +1,13 @@ +use actix_web::web; +use serde::Deserialize; + +use database::oauth2_client::OAuth2Client; + use crate::response_types::Empty; use crate::routes::appdata::WDatabase; use crate::routes::auth::Auth; -use crate::routes::error::{WebError, WebResult}; +use crate::routes::error::{WebErrorKind, WebResult}; use crate::routes::v1::MANAGE_SCOPE; -use actix_web::web; -use database::oauth2_client::OAuth2Client; -use serde::Deserialize; #[derive(Deserialize)] pub struct Request { @@ -18,12 +20,12 @@ pub async fn remove( payload: web::Json, ) -> WebResult { if !auth.has_scope(MANAGE_SCOPE) { - return Err(WebError::Forbidden); + return Err(WebErrorKind::Forbidden.into()); } let client = OAuth2Client::get_by_client_id(&database, &payload.client_id) .await? - .ok_or(WebError::NotFound)?; + .ok_or(WebErrorKind::NotFound)?; client.delete(&database).await?; Ok(Empty) diff --git a/server/wilford/src/routes/v1/user/list.rs b/server/wilford/src/routes/v1/user/list.rs index 8f8b88a..5b02d41 100644 --- a/server/wilford/src/routes/v1/user/list.rs +++ b/server/wilford/src/routes/v1/user/list.rs @@ -1,9 +1,10 @@ +use actix_web::web; +use serde::Serialize; + use crate::routes::appdata::WDatabase; use crate::routes::auth::Auth; -use crate::routes::error::{WebError, WebResult}; +use crate::routes::error::{WebErrorKind, WebResult}; use crate::routes::v1::MANAGE_SCOPE; -use actix_web::web; -use serde::Serialize; #[derive(Serialize)] pub struct Response { @@ -19,7 +20,7 @@ pub struct User { pub async fn list(database: WDatabase, auth: Auth) -> WebResult> { if !auth.has_scope(MANAGE_SCOPE) { - return Err(WebError::Forbidden); + return Err(WebErrorKind::Forbidden.into()); } let users = database::user::User::list(&database) diff --git a/server/wilford/src/routes/v1/user/permitted_scopes/add.rs b/server/wilford/src/routes/v1/user/permitted_scopes/add.rs index d72d0f1..ba2d649 100644 --- a/server/wilford/src/routes/v1/user/permitted_scopes/add.rs +++ b/server/wilford/src/routes/v1/user/permitted_scopes/add.rs @@ -1,11 +1,13 @@ +use actix_web::web; +use serde::Deserialize; + +use database::user::User; + use crate::response_types::Empty; use crate::routes::appdata::WDatabase; use crate::routes::auth::Auth; -use crate::routes::error::{WebError, WebResult}; +use crate::routes::error::{WebErrorKind, WebResult}; use crate::routes::v1::MANAGE_SCOPE; -use actix_web::web; -use database::user::User; -use serde::Deserialize; #[derive(Deserialize)] pub struct Payload { @@ -17,16 +19,16 @@ pub struct Payload { pub async fn add(database: WDatabase, auth: Auth, payload: web::Json) -> WebResult { if !auth.has_scope(MANAGE_SCOPE) { - return Err(WebError::Forbidden); + return Err(WebErrorKind::Forbidden.into()); } let user = User::get_by_id(&database, &payload.to) .await? - .ok_or(WebError::NotFound)?; + .ok_or(WebErrorKind::NotFound)?; let current_scopes = user.list_permitted_scopes(&database).await?; if current_scopes.contains(&payload.scope) { - return Err(WebError::BadRequest); + return Err(WebErrorKind::BadRequest.into()); } user.grant_permitted_scope(&database, &payload.scope) diff --git a/server/wilford/src/routes/v1/user/permitted_scopes/list.rs b/server/wilford/src/routes/v1/user/permitted_scopes/list.rs index 5aaf5c4..1e22a29 100644 --- a/server/wilford/src/routes/v1/user/permitted_scopes/list.rs +++ b/server/wilford/src/routes/v1/user/permitted_scopes/list.rs @@ -1,10 +1,12 @@ +use actix_web::web; +use serde::{Deserialize, Serialize}; + +use database::user::User; + use crate::routes::appdata::WDatabase; use crate::routes::auth::Auth; -use crate::routes::error::{WebError, WebResult}; +use crate::routes::error::{WebErrorKind, WebResult}; use crate::routes::v1::MANAGE_SCOPE; -use actix_web::web; -use database::user::User; -use serde::{Deserialize, Serialize}; #[derive(Serialize)] pub struct Response { @@ -23,12 +25,12 @@ pub async fn list( query: web::Query, ) -> WebResult> { if !auth.has_scope(MANAGE_SCOPE) { - return Err(WebError::Forbidden); + return Err(WebErrorKind::Forbidden.into()); } let user = User::get_by_id(&database, &query.user) .await? - .ok_or(WebError::NotFound)?; + .ok_or(WebErrorKind::NotFound)?; let scopes = user.list_permitted_scopes(&database).await?; Ok(web::Json(Response { scopes })) diff --git a/server/wilford/src/routes/v1/user/permitted_scopes/remove.rs b/server/wilford/src/routes/v1/user/permitted_scopes/remove.rs index 8c2d61c..83a3f08 100644 --- a/server/wilford/src/routes/v1/user/permitted_scopes/remove.rs +++ b/server/wilford/src/routes/v1/user/permitted_scopes/remove.rs @@ -1,11 +1,13 @@ +use actix_web::web; +use serde::Deserialize; + +use database::user::User; + use crate::response_types::Empty; use crate::routes::appdata::WDatabase; use crate::routes::auth::Auth; -use crate::routes::error::{WebError, WebResult}; +use crate::routes::error::{WebErrorKind, WebResult}; use crate::routes::v1::MANAGE_SCOPE; -use actix_web::web; -use database::user::User; -use serde::Deserialize; #[derive(Deserialize)] pub struct Request { @@ -21,16 +23,16 @@ pub async fn remove( payload: web::Json, ) -> WebResult { if !auth.has_scope(MANAGE_SCOPE) { - return Err(WebError::Forbidden); + return Err(WebErrorKind::Forbidden.into()); } let user = User::get_by_id(&database, &payload.from) .await? - .ok_or(WebError::NotFound)?; + .ok_or(WebErrorKind::NotFound)?; let current_scops = user.list_permitted_scopes(&database).await?; if !current_scops.contains(&payload.scope) { - return Err(WebError::NotFound); + return Err(WebErrorKind::NotFound.into()); } user.remove_permitted_scope(&database, &payload.scope) diff --git a/server/wilford/src/routes/well_known/jwks.rs b/server/wilford/src/routes/well_known/jwks.rs index bc16e49..3572ef0 100644 --- a/server/wilford/src/routes/well_known/jwks.rs +++ b/server/wilford/src/routes/well_known/jwks.rs @@ -1,6 +1,13 @@ -use crate::routes::appdata::WOidcPublicKey; use actix_web::web; -use serde::Serialize; +use base64::Engine; +use base64::prelude::BASE64_URL_SAFE_NO_PAD; +use rsa::{BigUint, RsaPublicKey}; +use rsa::pkcs8::DecodePublicKey; +use rsa::traits::PublicKeyParts; +use serde::{Serialize, Serializer}; + +use crate::routes::appdata::WOidcPublicKey; +use crate::routes::error::WebResult; #[derive(Serialize)] pub struct Jwks { @@ -12,16 +19,39 @@ pub struct Key { kty: String, r#use: String, alg: String, - k: String, + kid: String, + key_ops: Vec, + // k: String, + #[serde(serialize_with = "serialize")] + n: BigUint, + #[serde(serialize_with = "serialize")] + e: BigUint, } -pub async fn jwks(oidc_public_key: WOidcPublicKey) -> web::Json { - web::Json(Jwks { +pub async fn jwks(oidc_public_key: WOidcPublicKey) -> WebResult> { + let public_key = RsaPublicKey::from_public_key_pem(&oidc_public_key.0)?; + + Ok(web::Json(Jwks { keys: vec![Key { kty: "RSA".to_string(), - r#use: "verify".to_string(), + r#use: "sig".to_string(), alg: "RS256".to_string(), - k: (**oidc_public_key).0.clone(), + kid: "rsa".to_string(), // We dont have a kid + key_ops: vec![ + "verify".to_string(), + ], + // k: (**oidc_public_key).0.clone(), + n: public_key.n().clone(), + e: public_key.e().clone() }], - }) + })) } + +pub fn serialize(value: &BigUint, serializer: S) -> Result +where + S: Serializer, +{ + let bytes = value.to_bytes_be(); + let base64 = BASE64_URL_SAFE_NO_PAD.encode(bytes.as_slice()); + serializer.serialize_str(&base64) +} \ No newline at end of file diff --git a/server/wilford/src/routes/well_known/openid_configuration.rs b/server/wilford/src/routes/well_known/openid_configuration.rs index 210db9f..36fa282 100644 --- a/server/wilford/src/routes/well_known/openid_configuration.rs +++ b/server/wilford/src/routes/well_known/openid_configuration.rs @@ -10,6 +10,7 @@ pub struct OpenidConfiguration { response_types_supported: Vec, grant_types_supported: Vec, id_token_signing_alg_values_supported: Vec, + jwks_uri: String, } pub async fn openid_configuration(config: WConfig) -> web::Json { @@ -24,5 +25,6 @@ pub async fn openid_configuration(config: WConfig) -> web::Json