1
0
Fork 0
mirror of https://github.com/zhaofengli/attic.git synced 2024-12-14 11:57:30 +00:00

Merge pull request #177 from zhaofengli/rs256-support

Support RS256 JWTs
This commit is contained in:
Zhaofeng Li 2024-10-05 12:34:44 -06:00 committed by GitHub
commit 858120c450
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 509 additions and 142 deletions

2
Cargo.lock generated
View file

@ -325,6 +325,7 @@ dependencies = [
"maybe-owned",
"rand",
"regex",
"rsa 0.9.6",
"ryu",
"sea-orm",
"sea-orm-migration",
@ -355,6 +356,7 @@ dependencies = [
"jwt-simple",
"lazy_static",
"regex",
"rsa 0.9.6",
"serde",
"serde_with",
"tracing",

View file

@ -19,7 +19,7 @@ use crate::nix_store::StorePath;
/// Expected values for `nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps`.
pub const NO_DEPS: TestNar = TestNar {
store_path: "/nix/store/nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps",
original_file: include_bytes!("nar/nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps"),
_original_file: include_bytes!("nar/nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps"),
nar: include_bytes!("nar/nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps.nar"),
export: include_bytes!("nar/nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps.export"),
closure: &["nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps"],
@ -31,7 +31,7 @@ pub const NO_DEPS: TestNar = TestNar {
/// as `3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final`.
pub const WITH_DEPS_A: TestNar = TestNar {
store_path: "/nix/store/n7q4i7rlmbk4xz8qdsxpm6jbhrnxraq2-attic-test-with-deps-a",
original_file: include_bytes!("nar/n7q4i7rlmbk4xz8qdsxpm6jbhrnxraq2-attic-test-with-deps-a"),
_original_file: include_bytes!("nar/n7q4i7rlmbk4xz8qdsxpm6jbhrnxraq2-attic-test-with-deps-a"),
nar: include_bytes!("nar/n7q4i7rlmbk4xz8qdsxpm6jbhrnxraq2-attic-test-with-deps-a.nar"),
export: include_bytes!("nar/n7q4i7rlmbk4xz8qdsxpm6jbhrnxraq2-attic-test-with-deps-a.export"),
closure: &[
@ -46,7 +46,7 @@ pub const WITH_DEPS_A: TestNar = TestNar {
/// This depends on `3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final`.
pub const WITH_DEPS_B: TestNar = TestNar {
store_path: "/nix/store/544qcchwgcgpz3xi1bbml28f8jj6009p-attic-test-with-deps-b",
original_file: include_bytes!("nar/544qcchwgcgpz3xi1bbml28f8jj6009p-attic-test-with-deps-b"),
_original_file: include_bytes!("nar/544qcchwgcgpz3xi1bbml28f8jj6009p-attic-test-with-deps-b"),
nar: include_bytes!("nar/544qcchwgcgpz3xi1bbml28f8jj6009p-attic-test-with-deps-b.nar"),
export: include_bytes!("nar/544qcchwgcgpz3xi1bbml28f8jj6009p-attic-test-with-deps-b.export"),
closure: &[
@ -58,7 +58,7 @@ pub const WITH_DEPS_B: TestNar = TestNar {
/// Expected values for `3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final`.
pub const WITH_DEPS_C: TestNar = TestNar {
store_path: "/nix/store/3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final",
original_file: include_bytes!(
_original_file: include_bytes!(
"nar/3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final"
),
nar: include_bytes!("nar/3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final.nar"),
@ -75,7 +75,7 @@ pub struct TestNar {
store_path: &'static str,
/// The original file.
original_file: &'static [u8],
_original_file: &'static [u8],
/// A NAR dump without path metadata.
nar: &'static [u8],

View file

@ -11,16 +11,16 @@ Attic provides [a NixOS module](https://github.com/zhaofengli/attic/blob/main/ni
## Generating the Credentials File
The HS256 JWT secret can be generated with the `openssl` utility:
The RS256 JWT secret can be generated with the `openssl` utility:
```bash
openssl rand 64 | base64 -w0
nix run nixpkgs#openssl -- genrsa -traditional 4096 | base64 -w0
```
Create a file on the server containing the following contents:
```
ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="output from openssl"
ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64="output from above"
```
Ensure the file is only accessible by root.
@ -47,6 +47,8 @@ You can import the module in one of two ways:
settings = {
listen = "[::]:8080";
jwt = { };
# Data chunking
#
# Warning: If you change any of the values here, it will be

View file

@ -5,8 +5,8 @@ let
serverConfigFile = config.nodes.server.services.atticd.configFile;
cmd = {
atticadm = "atticd-atticadm";
atticd = ". /etc/atticd.env && export ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64 && atticd -f ${serverConfigFile}";
atticadm = ". /etc/atticd.env && export ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64 && atticd-atticadm";
atticd = ". /etc/atticd.env && export ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64 && atticd -f ${serverConfigFile}";
};
makeTestDerivation = pkgs.writeShellScript "make-drv" ''
@ -147,7 +147,7 @@ in {
# For testing only - Don't actually do this
environment.etc."atticd.env".text = ''
ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="dGVzdCBzZWNyZXQ="
ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64='LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBekhqUzFGKzlRaFFUdlJZYjZ0UGhxS09FME5VYkIraTJMOTByWVBNQVVoYVBUMmlKCmVUNk9vWFlmZWszZlZ1dXIrYks1VWFVRjhUbEx2Y1FHa1Arckd0WDRiQUpGTWJBcTF3Y25FQ3R6ZGVERHJnSlIKMGUvNWJhdXQwSS9YS0ticG9oYjNvWVhtUmR5eG9WVGE3akY1bk11ajBsd25kUTcwYTF1ZGkzMGNpYkdTWHZMagpVeGltL3ByYjUrV3ZPdjN4UnhlbDZHYmptUW1RMVBHeHVLcmx3b1ZKRnlWTjl3QmExajBDelJDcURnTFRwQWw0CjhLVWlDY2V1VUZQcmdZaW9vSVhyVExlWmxVbFVVV3FHSDBJbGFKeVUyQ05iNWJtZWM1TnZ4RDlaakFoYytucmgKRS80VzkxajdQMFVyQnp4am9NUTRlKzBPZDhmQnBvSDAwbm4xUXdJREFRQUJBb0lCQUE2RmxEK21Ed3gyM1pJRAoxSGJBbHBuQ0IwaEhvbFJVK0Q5OC96d3k5ZlplaU00VWVCTUcyTjFweE1HTWIweStqeWU4UkVJaXJNSGRsbDRECllvNEF3bmUwODZCRUp3TG81cG4vOVl2RjhqelFla1ZNLzkrZm9nRGlmUVUvZWdIMm5NZzR4bHlQNUhOWXdicmEKQ25SNVNoQlRQQzdRQWJOa0hRTFU3bUwrUHowZUlXaG9KWVRoUUpkU0g3RDB0K1QwZzVVNDdPam5qbXJaTWwxaApHOE1IUHhKMk5WU1l2N0dobnpjblZvcVVxYzlxeldXRDZXZERtV1BPNGJ1K2p0b2E2U2o4cjJtb0RRZ1A5YXNhCm93RUFJbHBmbVkxYUx2dENwWG4rejRTTWJKcHRXMlVvaktGa2dkYm9jZmtXYWdtSGZRa2xmS0dBQ0hibU9ZV24KeDRCbTU3a0NnWUVBN1dXaXJDZnBRR01hR3A2WWxMQlVUc1VJSXJOclF4UmtuRlc3dFVYd0NqWFZ5SDlTR3FqNgphTkNhYzZpaks3QVNBYXlxY1JQRjFPY2gyNmxpVmRKUHNuRGxwUjhEVXB2TzRVOVRzSTJyZ1lZYzNrSWkzVGFKClgzV0Vic1Z6Nk45WXFPSXlnVnZiTEhLS0F4Uyt4b1Z2SjkzQmdWRHN5SkxRdmhrM3VubXk3M2tDZ1lFQTNINnYKeUhOKzllOVAyOS9zMVY1eWZxSjdvdVdKV0lBTHFDYm9zOTRRSVdPSG5HRUtSSGkydWIzR0d6U2tRSzN1eTUrdQo4M0txaFJOejRVMkdOK1pLaFE0NHhNVmV4TUVvZzJVU3lTaVZ0cFdqWXBwT2Q1NnVaMzRWaFU2TWRNZS9zT0JnCnNoei84MUxUSis2cHdFZE9wV2tPVlRaMXJISlZXQmdtVk5qWjc1c0NnWUVBNVd5YjBaU2dyMEVYTVRLa2NzNFcKTENudXV0cDZodEZtaWsrd29IZCtpOStMUThFSU1BdXVOUzJrbHJJYlAxVmhrWXkxQzZMNFJkRTV2M2ZyT05XUApmL3ZyYzdDTkhZREdacWlyVUswWldvdXB5b0pQLzBsOWFXdkJHT3hxSUZ2NDZ2M3ZvV1NNWkdBdFVOenpvaGZDClhOeks3WmF2dndka0JOT0tNQVQ5RU1FQ2dZRUF3NEhaWDRWNUo1d2dWVGVDQ2RjSzhsb2tBbFpBcUNZeEw5SUEKTjZ4STVUSVpSb0dNMXhXcC81dlRrci9rZkMwOU5YUExiclZYbVZPY1JrTzFKTStmZDhjYWN1OEdqck11dHdMaAoyMWVQR0N3cWlQMkZZZTlqZVFTRkZJU0hhZXpMZll3V2NSZmhvdURudGRxYXpaRHNuU0kvd1RMZXVCOVFxU0lRCnF0NzByczBDZ1lCQ2lzV0VKdXpQUUlJNzVTVkU4UnJFZGtUeUdhOEVBOHltcStMdDVLRDhPYk80Q2JHYVFlWXkKWFpjSHVyOFg2cW1lWHZVU3MwMHBMMUdnTlJ3WCtSUjNMVDhXTm9vc0NqVDlEUW9GOFZveEtseDROVTRoUGlrTQpBc0w1RS9wYnVLeXkvSU5LTnQyT3ZPZmJYVitlTXZQdGs5c1dORjNyRTBYcU15TW9maG9NaVE9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo='
'';
services.atticd = {
@ -156,6 +156,8 @@ in {
settings = {
listen = "[::]:8080";
jwt = { };
chunking = {
nar-size-threshold = 1;
min-size = 64 * 1024;
@ -165,7 +167,7 @@ in {
};
};
environment.systemPackages = [ pkgs.attic-server ];
environment.systemPackages = [ pkgs.openssl pkgs.attic-server ];
networking.firewall.allowedTCPPorts = [ 8080 ];
};

View file

@ -16,7 +16,7 @@ let
} ''
cat $configFile
export ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="dGVzdCBzZWNyZXQ="
export ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64="$(${pkgs.openssl}/bin/openssl genrsa -traditional 4096 | ${pkgs.coreutils}/bin/base64 -w0)"
export ATTIC_SERVER_DATABASE_URL="sqlite://:memory:"
${cfg.package}/bin/atticd --mode check-config -f $configFile
cat <$configFile >$out
@ -79,8 +79,8 @@ in
Path to an EnvironmentFile containing required environment
variables:
- ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64: The Base64-encoded version of the
HS256 JWT secret. Generate it with `openssl rand 64 | base64 -w0`.
- ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64: The base64-encoded RSA PEM PKCS1 of the
RS256 JWT secret. Generate it with `openssl genrsa -traditional 4096 | base64 -w0`.
'';
type = types.nullOr types.path;
default = null;
@ -154,9 +154,9 @@ in
message = ''
<option>services.atticd.credentialsFile</option> is not set.
Run `openssl rand 64 | base64 -w0` and create a file with the following contents:
Run `openssl genrsa -traditional -out private_key.pem 4096 | base64 -w0` and create a file with the following contents:
ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="output from command"
ATTIC_SERVER_TOKEN_RS256_SECRET="output from command"
Then, set `services.atticd.credentialsFile` to the quoted absolute path of the file.
'';

View file

@ -60,6 +60,7 @@ tracing-subscriber = { version = "0.3.17", features = [ "json" ] }
uuid = { version = "1.3.3", features = ["v4"] }
console-subscriber = "0.2.0"
xdg = "2.5.0"
rsa = "0.9.3"
[dependencies.async-compression]
version = "0.4.0"

View file

@ -1,5 +1,7 @@
//! HTTP middlewares for access control.
use attic::cache::CacheName;
use attic_token::util::parse_authorization_header;
use axum::{extract::Request, middleware::Next, response::Response};
use sea_orm::DatabaseConnection;
use tokio::sync::OnceCell;
@ -8,8 +10,6 @@ use crate::access::{CachePermission, Token};
use crate::database::{entity::cache::CacheModel, AtticDatabase};
use crate::error::ServerResult;
use crate::{RequestState, State};
use attic::cache::CacheName;
use attic_token::util::parse_authorization_header;
/// Auth state.
#[derive(Debug)]
@ -101,10 +101,19 @@ pub async fn apply_auth(req: Request, next: Next) -> Response {
.and_then(parse_authorization_header)
.and_then(|jwt| {
let state = req.extensions().get::<State>().unwrap();
let res_token = Token::from_jwt(&jwt, &state.config.token_hs256_secret);
let signature_type = state.config.jwt.signing_config.clone().into();
let res_token = Token::from_jwt(
&jwt,
&signature_type,
&state.config.jwt.token_bound_issuer,
&state.config.jwt.token_bound_audiences,
);
if let Err(e) = &res_token {
tracing::debug!("Ignoring bad JWT token: {}", e);
}
res_token.ok()
});

View file

@ -115,7 +115,13 @@ pub async fn run(config: Config, opts: Opts) -> Result<()> {
if sub.dump_claims {
println!("{}", serde_json::to_string(token.opaque_claims())?);
} else {
let encoded_token = token.encode(&config.token_hs256_secret)?;
let signature_type = config.jwt.signing_config.into();
let encoded_token = token.encode(
&signature_type,
&config.jwt.token_bound_issuer,
&config.jwt.token_bound_audiences,
)?;
println!("{}", encoded_token);
}

View file

@ -34,13 +34,6 @@ allowed-hosts = []
# cache.
#require-proof-of-possession = true
# JWT signing token
#
# Set this to the Base64 encoding of some random data.
# You can also set it via the `ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64` environment
# variable.
token-hs256-secret-base64 = "%token_hs256_secret_base64%"
# Database connection
[database]
# Connection URL
@ -85,7 +78,7 @@ path = "%storage_path%"
#[storage.credentials]
# access_key_id = ""
# secret_access_key = ""
# Data chunking
#
# Warning: If you change any of the values here, it will be
@ -134,3 +127,37 @@ interval = "12 hours"
# Zero (default) means time-based garbage-collection is
# disabled by default. You can enable it on a per-cache basis.
#default-retention-period = "6 months"
[jwt]
# WARNING: Changing _anything_ in this section will break any existing
# tokens. If you need to regenerate them, ensure that you use the the
# correct secret and include the `iss` and `aud` claims.
# JWT `iss` claim
#
# Set this to the JWT issuer that you want to validate.
# If this is set, all received JWTs will validate that the `iss` claim
# matches this value.
#token-bound-issuer = "some-issuer"
# JWT `aud` claim
#
# Set this to the JWT audience(s) that you want to validate.
# If this is set, all received JWTs will validate that the `aud` claim
# contains at least one of these values.
#token-bound-audiences = ["some-audience1", "some-audience2"]
[jwt.signing]
# JWT RS256 secret key
#
# Set this to the base64-encoded private half of an RSA PEM PKCS1 key.
# You can also set it via the `ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64`
# environment variable.
token-rs256-secret-base64 = "%token_rs256_secret_base64%"
# JWT HS256 secret key
#
# Set this to the base64-encoded HMAC secret key.
# You can also set it via the `ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64`
# environment variable.
#token-hs256-secret-base64 = ""

View file

@ -1,5 +1,6 @@
//! Server configuration.
use std::collections::HashSet;
use std::env;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
@ -7,12 +8,16 @@ use std::time::Duration;
use anyhow::Result;
use async_compression::Level as CompressionLevel;
use attic_token::SignatureType;
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
use derivative::Derivative;
use serde::{de, Deserialize};
use xdg::BaseDirectories;
use crate::access::{decode_token_hs256_secret_base64, HS256Key};
use crate::access::{
decode_token_hs256_secret_base64, decode_token_rs256_pubkey_base64,
decode_token_rs256_secret_base64, HS256Key, RS256KeyPair, RS256PublicKey,
};
use crate::narinfo::Compression as NixCompression;
use crate::storage::{LocalStorageConfig, S3StorageConfig};
@ -26,9 +31,18 @@ const XDG_PREFIX: &str = "attic";
/// This is useful for deploying to certain application platforms like Fly.io
const ENV_CONFIG_BASE64: &str = "ATTIC_SERVER_CONFIG_BASE64";
/// Environment variable storing the Base64-encoded HS256 JWT secret.
/// Environment variable storing the base64-encoded HMAC secret (used for signing and verifying
/// received JWTs).
const ENV_TOKEN_HS256_SECRET_BASE64: &str = "ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64";
/// Environment variable storing the base64-encoded RSA PEM PKCS1 private key (used for signing and
/// verifying received JWTs).
const ENV_TOKEN_RS256_SECRET_BASE64: &str = "ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64";
/// Environment variable storing the base64-encoded RSA PEM PKCS1 public key (used for verifying
/// received JWTs only).
const ENV_TOKEN_RS256_PUBKEY_BASE64: &str = "ATTIC_SERVER_TOKEN_RS256_PUBKEY_BASE64";
/// Environment variable storing the database connection string.
const ENV_DATABASE_URL: &str = "ATTIC_SERVER_DATABASE_URL";
@ -108,14 +122,81 @@ pub struct Config {
#[serde(default = "Default::default")]
pub garbage_collection: GarbageCollectionConfig,
/// JSON Web Token.
pub jwt: JWTConfig,
/// (Deprecated Stub)
///
/// This simply results in an error telling the user to update
/// their configuration.
#[serde(rename = "token-hs256-secret-base64")]
#[serde(default = "Default::default")]
#[serde(deserialize_with = "deserialize_deprecated_token_hs256_secret")]
#[derivative(Debug = "ignore")]
pub _depreated_token_hs256_secret: Option<String>,
}
/// JSON Web Token configuration.
#[derive(Clone, Derivative, Deserialize)]
#[derivative(Debug)]
pub struct JWTConfig {
/// The `iss` claim of the JWT.
///
/// If specified, received JWTs must have this claim, and its value must match this
/// configuration.
#[serde(rename = "token-bound-issuer")]
#[serde(default = "Default::default")]
pub token_bound_issuer: Option<String>,
/// The `aud` claim of the JWT.
///
/// If specified, received JWTs must have this claim, and must contain one of the configured
/// values.
#[serde(rename = "token-bound-audiences")]
#[serde(default = "Default::default")]
pub token_bound_audiences: Option<HashSet<String>>,
/// JSON Web Token signing.
#[serde(rename = "signing")]
#[serde(default = "load_jwt_signing_config_from_env")]
#[derivative(Debug = "ignore")]
pub signing_config: JWTSigningConfig,
}
/// JSON Web Token signing configuration.
#[derive(Clone, Deserialize)]
pub enum JWTSigningConfig {
/// JSON Web Token RSA pubkey.
///
/// Set this to the base64-encoded RSA PEM PKCS1 public key to use for verifying JWTs only.
#[serde(rename = "token-rs256-pubkey-base64")]
#[serde(deserialize_with = "deserialize_token_rs256_pubkey_base64")]
RS256VerifyOnly(RS256PublicKey),
/// JSON Web Token RSA secret.
///
/// Set this to the base64-encoded RSA PEM PKCS1 private key to use for signing and verifying
/// JWTs.
#[serde(rename = "token-rs256-secret-base64")]
#[serde(deserialize_with = "deserialize_token_rs256_secret_base64")]
RS256SignAndVerify(RS256KeyPair),
/// JSON Web Token HMAC secret.
///
/// Set this to the base64 encoding of a randomly generated secret.
/// Set this to the base64-encoded HMAC secret to use for signing and verifying JWTs.
#[serde(rename = "token-hs256-secret-base64")]
#[serde(deserialize_with = "deserialize_token_hs256_secret_base64")]
#[serde(default = "load_token_hs256_secret_from_env")]
#[derivative(Debug = "ignore")]
pub token_hs256_secret: HS256Key,
HS256SignAndVerify(HS256Key),
}
impl From<JWTSigningConfig> for SignatureType {
fn from(value: JWTSigningConfig) -> Self {
match value {
JWTSigningConfig::RS256VerifyOnly(key) => Self::RS256PubkeyOnly(key),
JWTSigningConfig::RS256SignAndVerify(key) => Self::RS256(key),
JWTSigningConfig::HS256SignAndVerify(key) => Self::HS256(key),
}
}
}
/// Database connection configuration.
@ -240,16 +321,82 @@ pub struct GarbageCollectionConfig {
pub default_retention_period: Duration,
}
fn load_token_hs256_secret_from_env() -> HS256Key {
let s = env::var(ENV_TOKEN_HS256_SECRET_BASE64)
.expect("The HS256 secret must be specified in either token_hs256_secret or the ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64 environment.");
fn load_jwt_signing_config_from_env() -> JWTSigningConfig {
let config = if let Some(config) = load_token_rs256_pubkey_from_env() {
config
} else if let Some(config) = load_token_rs256_secret_from_env() {
config
} else if let Some(config) = load_token_hs256_secret_from_env() {
config
} else {
panic!(
"\n\
You must configure JWT signing and verification inside your TOML \
configuration by setting one of the following options in the \
[jwt.signing] block:\n\
\n\
* token-rs256-pubkey-base64\n\
* token-rs256-secret-base64\n\
* token-hs256-secret-base64\n\
\n\
or by setting one of the following environment variables:\n\
\n\
* {ENV_TOKEN_RS256_PUBKEY_BASE64}\n\
* {ENV_TOKEN_RS256_SECRET_BASE64}\n\
* {ENV_TOKEN_HS256_SECRET_BASE64}\n\
\n\
Options will be tried in that same order (configuration options \
first, then environment options if none of the configuration options \
were set, starting with the respective RSA pubkey option, the RSA \
secret option, and finally the HMAC secret option). \
The first option that is found will be used.\n\
\n\
If an RS256 pubkey (asymmetric RSA PEM PKCS1 public key) is \
provided, it will only be possible to verify received JWTs, and not \
sign new JWTs.\n\
\n\
If an RS256 secret (asymmetric RSA PEM PKCS1 private key) is \
provided, it will be used for both signing new JWTs and verifying \
received JWTs.\n\
\n\
If an HS256 secret (symmetric HMAC secret) is provided, it will be \
used for both signing new JWTs and verifying received JWTs.\n\
"
)
};
decode_token_hs256_secret_base64(&s).expect("Failed to load as decoding key")
config
}
fn load_token_hs256_secret_from_env() -> Option<JWTSigningConfig> {
let s = env::var(ENV_TOKEN_HS256_SECRET_BASE64).ok()?;
decode_token_hs256_secret_base64(&s)
.ok()
.map(JWTSigningConfig::HS256SignAndVerify)
}
fn load_token_rs256_secret_from_env() -> Option<JWTSigningConfig> {
let s = env::var(ENV_TOKEN_RS256_SECRET_BASE64).ok()?;
decode_token_rs256_secret_base64(&s)
.ok()
.map(JWTSigningConfig::RS256SignAndVerify)
}
fn load_token_rs256_pubkey_from_env() -> Option<JWTSigningConfig> {
let s = env::var(ENV_TOKEN_RS256_PUBKEY_BASE64).ok()?;
decode_token_rs256_pubkey_base64(&s)
.ok()
.map(JWTSigningConfig::RS256VerifyOnly)
}
fn load_database_url_from_env() -> String {
env::var(ENV_DATABASE_URL)
.expect("Database URL must be specified in either database.url or the ATTIC_SERVER_DATABASE_URL environment.")
env::var(ENV_DATABASE_URL).expect(&format!(
"Database URL must be specified in either database.url \
or the {ENV_DATABASE_URL} environment."
))
}
impl CompressionConfig {
@ -296,6 +443,30 @@ impl Default for GarbageCollectionConfig {
}
}
fn deserialize_deprecated_token_hs256_secret<'de, D>(
_deserializer: D,
) -> Result<Option<String>, D::Error>
where
D: de::Deserializer<'de>,
{
use de::Error;
Err(Error::custom(
"\n\
The token-hs256-secret-base64 field has been moved to [jwt.signing].\n\
\n\
To continue using HS256 signing, move your current config:\n\
\n\
token-hs256-secret-base64 = \"your token\"\n\
\n\
To the bottom of the file like so:\n\
\n\
[jwt.signing]\n\
token-hs256-secret-base64 = \"your token\"\n\
",
))
}
fn deserialize_token_hs256_secret_base64<'de, D>(deserializer: D) -> Result<HS256Key, D::Error>
where
D: de::Deserializer<'de>,
@ -308,6 +479,32 @@ where
Ok(key)
}
fn deserialize_token_rs256_secret_base64<'de, D>(deserializer: D) -> Result<RS256KeyPair, D::Error>
where
D: de::Deserializer<'de>,
{
use de::Error;
let s = String::deserialize(deserializer)?;
let key = decode_token_rs256_secret_base64(&s).map_err(Error::custom)?;
Ok(key)
}
fn deserialize_token_rs256_pubkey_base64<'de, D>(
deserializer: D,
) -> Result<RS256PublicKey, D::Error>
where
D: de::Deserializer<'de>,
{
use de::Error;
let s = String::deserialize(deserializer)?;
let key = decode_token_rs256_pubkey_base64(&s).map_err(Error::custom)?;
Ok(key)
}
fn default_listen_address() -> SocketAddr {
"[::]:8080".parse().unwrap()
}

View file

@ -14,11 +14,10 @@
use anyhow::Result;
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
use chrono::{Months, Utc};
use rand::distributions::Alphanumeric;
use rand::Rng;
use rsa::pkcs1::EncodeRsaPrivateKey;
use tokio::fs::{self, OpenOptions};
use crate::access::{decode_token_hs256_secret_base64, Token};
use crate::access::{decode_token_rs256_secret_base64, SignatureType, Token};
use crate::config;
use attic::cache::CacheNamePattern;
@ -45,20 +44,18 @@ pub async fn run_oobe() -> Result<()> {
let storage_path = data_path.join("storage");
fs::create_dir_all(&storage_path).await?;
let hs256_secret_base64 = {
let random: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(128)
.map(char::from)
.collect();
let rs256_secret_base64 = {
let mut rng = rand::thread_rng();
let private_key = rsa::RsaPrivateKey::new(&mut rng, 4096)?;
let pkcs1_pem = private_key.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF)?;
BASE64_STANDARD.encode(random)
BASE64_STANDARD.encode(pkcs1_pem.as_bytes())
};
let config_content = CONFIG_TEMPLATE
.replace("%database_url%", &database_url)
.replace("%storage_path%", storage_path.to_str().unwrap())
.replace("%token_hs256_secret_base64%", &hs256_secret_base64);
.replace("%token_rs256_secret_base64%", &rs256_secret_base64);
fs::write(&config_path, config_content.as_bytes()).await?;
@ -76,8 +73,8 @@ pub async fn run_oobe() -> Result<()> {
perm.configure_cache_retention = true;
perm.destroy_cache = true;
let key = decode_token_hs256_secret_base64(&hs256_secret_base64).unwrap();
token.encode(&key)?
let key = decode_token_rs256_secret_base64(&rs256_secret_base64).unwrap();
token.encode(&SignatureType::RS256(key), &None, &None)?
};
eprintln!();

View file

@ -9,7 +9,7 @@ edition = "2021"
attic = { path = "../attic", default-features = false }
base64 = "0.22.1"
chrono = "0.4.24"
chrono = "0.4.31"
displaydoc = "0.2.4"
indexmap = { version = "2.2.6", features = ["serde"] }
jwt-simple = "0.11.5"
@ -18,3 +18,4 @@ regex = "1.8.3"
serde = "1.0.163"
serde_with = "3.0.0"
tracing = "0.1.37"
rsa = "0.9.3"

View file

@ -1,7 +1,7 @@
//! Access control.
//!
//! Access control in Attic is simple and stateless [0] - The server validates
//! the JWT against a HS256 key and allows access based on the `https://jwt.attic.rs/v1`
//! the JWT against the configured key and allows access based on the `https://jwt.attic.rs/v1`
//! claim.
//!
//! One primary goal of the Attic Server is easy scalability. It's designed
@ -83,14 +83,16 @@ pub mod util;
#[cfg(test)]
mod tests;
use std::collections::HashSet;
use std::error::Error as StdError;
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
use chrono::{DateTime, Utc};
use displaydoc::Display;
use indexmap::IndexMap;
use jwt_simple::prelude::{Duration, RSAKeyPairLike, RSAPublicKeyLike, VerificationOptions};
pub use jwt_simple::{
algorithms::{HS256Key, MACLike},
algorithms::{HS256Key, MACLike, RS256KeyPair, RS256PublicKey},
claims::{Claims, JWTClaims},
prelude::UnixTimeStamp,
};
@ -155,49 +157,49 @@ pub struct AtticAccess {
pub struct CachePermission {
/// Can pull objects from the cache.
#[serde(default = "CachePermission::permission_default")]
#[serde(skip_serializing_if = "is_false")]
#[serde(skip_serializing_if = "std::ops::Not::not")]
#[serde(rename = "r")]
#[serde_as(as = "BoolFromInt")]
pub pull: bool,
/// Can push objects to the cache.
#[serde(default = "CachePermission::permission_default")]
#[serde(skip_serializing_if = "is_false")]
#[serde(skip_serializing_if = "std::ops::Not::not")]
#[serde(rename = "w")]
#[serde_as(as = "BoolFromInt")]
pub push: bool,
/// Can delete objects from the cache.
#[serde(default = "CachePermission::permission_default")]
#[serde(skip_serializing_if = "is_false")]
#[serde(skip_serializing_if = "std::ops::Not::not")]
#[serde(rename = "d")]
#[serde_as(as = "BoolFromInt")]
pub delete: bool,
/// Can create the cache itself.
#[serde(default = "CachePermission::permission_default")]
#[serde(skip_serializing_if = "is_false")]
#[serde(skip_serializing_if = "std::ops::Not::not")]
#[serde(rename = "cc")]
#[serde_as(as = "BoolFromInt")]
pub create_cache: bool,
/// Can reconfigure the cache.
#[serde(default = "CachePermission::permission_default")]
#[serde(skip_serializing_if = "is_false")]
#[serde(skip_serializing_if = "std::ops::Not::not")]
#[serde(rename = "cr")]
#[serde_as(as = "BoolFromInt")]
pub configure_cache: bool,
/// Can configure retention/quota settings.
#[serde(default = "CachePermission::permission_default")]
#[serde(skip_serializing_if = "is_false")]
#[serde(skip_serializing_if = "std::ops::Not::not")]
#[serde(rename = "cq")]
#[serde_as(as = "BoolFromInt")]
pub configure_cache_retention: bool,
/// Can destroy the cache itself.
#[serde(default = "CachePermission::permission_default")]
#[serde(skip_serializing_if = "is_false")]
#[serde(skip_serializing_if = "std::ops::Not::not")]
#[serde(rename = "cd")]
#[serde_as(as = "BoolFromInt")]
pub destroy_cache: bool,
@ -223,14 +225,68 @@ pub enum Error {
/// Base64 decode error: {0}
Base64Error(base64::DecodeError),
/// RSA Key error: {0}
RsaKeyError(rsa::pkcs1::Error),
/// Failure decoding the base64 layer of the base64 encoded PEM
Utf8Error(std::str::Utf8Error),
/// Pubkey-only JWT authentication cannot create signed JWTs
PubkeyOnlyCannotCreateToken,
}
/// The supported JWT signature types.
pub enum SignatureType {
HS256(HS256Key),
RS256(RS256KeyPair),
RS256PubkeyOnly(RS256PublicKey),
}
impl Token {
/// Verifies and decodes a token.
pub fn from_jwt(token: &str, key: &HS256Key) -> Result<Self> {
key.verify_token(token, None)
.map_err(Error::TokenError)
.map(Token)
pub fn from_jwt(
token: &str,
signature_type: &SignatureType,
maybe_bound_issuer: &Option<String>,
maybe_bound_audiences: &Option<HashSet<String>>,
) -> Result<Self> {
let opts = VerificationOptions {
reject_before: None,
accept_future: false,
required_subject: None,
required_key_id: None,
required_public_key: None,
required_nonce: None,
allowed_issuers: maybe_bound_issuer
.as_ref()
.map(|s| [s.to_owned()].into())
.to_owned(),
allowed_audiences: maybe_bound_audiences.to_owned(),
time_tolerance: None,
max_validity: None,
max_token_length: None,
max_header_length: None,
artificial_time: None,
};
match signature_type {
SignatureType::HS256(key) => key
.verify_token(token, Some(opts))
.map_err(Error::TokenError)
.map(Token),
SignatureType::RS256(key) => {
let public_key = key.public_key();
public_key
.verify_token(token, Some(opts))
.map_err(Error::TokenError)
.map(Token)
}
SignatureType::RS256PubkeyOnly(key) => key
.verify_token(token, Some(opts))
.map_err(Error::TokenError)
.map(Token),
}
}
/// Creates a new token with an expiration timestamp.
@ -239,12 +295,17 @@ impl Token {
attic_ns: Default::default(),
};
let now_epoch = Utc::now().signed_duration_since(DateTime::UNIX_EPOCH);
Self(JWTClaims {
issued_at: None,
expires_at: Some(UnixTimeStamp::from_secs(
exp.timestamp().try_into().unwrap(),
)),
invalid_before: None,
invalid_before: Some(Duration::new(
now_epoch.num_seconds().try_into().unwrap(),
0,
)),
issuer: None,
subject: Some(sub),
audiences: None,
@ -255,8 +316,28 @@ impl Token {
}
/// Encodes the token.
pub fn encode(&self, key: &HS256Key) -> Result<String> {
key.authenticate(self.0.clone()).map_err(Error::TokenError)
pub fn encode(
&self,
signature_type: &SignatureType,
maybe_bound_issuer: &Option<String>,
maybe_bound_audiences: &Option<HashSet<String>>,
) -> Result<String> {
let mut token = self.0.clone();
if let Some(issuer) = maybe_bound_issuer {
token = token.with_issuer(issuer);
}
if let Some(audiences) = maybe_bound_audiences {
token = token.with_audiences(audiences.to_owned());
}
match signature_type {
SignatureType::HS256(key) => key.authenticate(token).map_err(Error::TokenError),
SignatureType::RS256(key) => key.sign(token).map_err(Error::TokenError),
SignatureType::RS256PubkeyOnly(_) => {
return Err(Error::PubkeyOnlyCannotCreateToken);
}
}
}
/// Returns the subject of the token.
@ -362,11 +443,23 @@ impl CachePermission {
impl StdError for Error {}
pub fn decode_token_hs256_secret_base64(s: &str) -> Result<HS256Key> {
let secret = BASE64_STANDARD.decode(s).map_err(Error::Base64Error)?;
Ok(HS256Key::from_bytes(&secret))
let decoded = BASE64_STANDARD.decode(s).map_err(Error::Base64Error)?;
let secret = std::str::from_utf8(&decoded).map_err(Error::Utf8Error)?;
Ok(HS256Key::from_bytes(&secret.as_bytes()))
}
// bruh
fn is_false(b: &bool) -> bool {
!b
pub fn decode_token_rs256_secret_base64(s: &str) -> Result<RS256KeyPair> {
let decoded = BASE64_STANDARD.decode(s).map_err(Error::Base64Error)?;
let secret = std::str::from_utf8(&decoded).map_err(Error::Utf8Error)?;
let keypair = RS256KeyPair::from_pem(secret).map_err(Error::TokenError)?;
Ok(keypair)
}
pub fn decode_token_rs256_pubkey_base64(s: &str) -> Result<RS256PublicKey> {
let decoded = BASE64_STANDARD.decode(s).map_err(Error::Base64Error)?;
let pubkey = std::str::from_utf8(&decoded).map_err(Error::Utf8Error)?;
let pubkey = RS256PublicKey::from_pem(pubkey).map_err(Error::TokenError)?;
Ok(pubkey)
}

View file

@ -10,15 +10,12 @@ macro_rules! cache {
#[test]
fn test_basic() {
// "very secure secret"
let base64_secret = "dmVyeSBzZWN1cmUgc2VjcmV0";
let dec_key = decode_token_hs256_secret_base64(base64_secret).unwrap();
/*
$ cat json
{
"sub": "meow",
"exp": 4102324986,
"nbf": 0,
"https://jwt.attic.rs/v1": {
"caches": {
"all-*": {"r":1},
@ -31,69 +28,102 @@ fn test_basic() {
}
*/
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjQxMDIzMjQ5ODYsImh0dHBzOi8vand0LmF0dGljLnJzL3YxIjp7ImNhY2hlcyI6eyJhbGwtKiI6eyJyIjoxfSwiYWxsLWNpLSoiOnsidyI6MX0sImNhY2hlLXJvIjp7InIiOjF9LCJjYWNoZS1ydyI6eyJyIjoxLCJ3IjoxfSwidGVhbS0qIjp7ImNjIjoxLCJyIjoxLCJ3IjoxfX19LCJpYXQiOjE3MTY2NjA1ODksInN1YiI6Im1lb3cifQ.8vtxp_1OEYdcnkGPM4c9ORXooJZV7DOTS4NRkMKN8mw";
let tokens: &[(&str, Box<dyn Fn() -> Token>)] = &[
(
"hs256",
Box::new(|| {
// "very secure secret"
let base64_secret = "dmVyeSBzZWN1cmUgc2VjcmV0";
let dec_key = decode_token_hs256_secret_base64(base64_secret).unwrap();
// NOTE(cole-h): check that we get a consistent iteration order when getting permissions for
// caches -- this depends on the order of the fields in the token, but should otherwise be
// consistent between iterations
let mut was_ever_wrong = false;
for _ in 0..=1_000 {
// NOTE(cole-h): we construct a new Token every iteration in order to get different "random
// state"
let decoded = Token::from_jwt(token, &dec_key).unwrap();
let perm_all_ci = decoded.get_permission_for_cache(&cache! { "all-ci-abc" });
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjQxMDIzMjQ5ODYsImh0dHBzOi8vand0LmF0dGljLnJzL3YxIjp7ImNhY2hlcyI6eyJhbGwtKiI6eyJyIjoxfSwiYWxsLWNpLSoiOnsidyI6MX0sImNhY2hlLXJvIjp7InIiOjF9LCJjYWNoZS1ydyI6eyJyIjoxLCJ3IjoxfSwidGVhbS0qIjp7ImNjIjoxLCJyIjoxLCJ3IjoxfX19LCJpYXQiOjE3MTY2NjA1ODksInN1YiI6Im1lb3cifQ.8vtxp_1OEYdcnkGPM4c9ORXooJZV7DOTS4NRkMKN8mw";
// NOTE(cole-h): if the iteration order of the token is inconsistent, the permissions may be
// retrieved from the `all-ci-*` pattern (which only allows writing/pushing), even though
// the `all-*` pattern (which only allows reading/pulling) is specified first
if perm_all_ci.require_pull().is_err() || perm_all_ci.require_push().is_ok() {
was_ever_wrong = true;
Token::from_jwt(token, &SignatureType::HS256(dec_key), &None, &None).unwrap()
}),
),
(
"rs256",
Box::new(|| {
// nix shell nixpkgs#jwt-cli
// openssl genpkey -out rs256 -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -outform der
// BASE64_SECRET=$(openssl rsa -in rs256 -outform PEM -traditional | base64 -w0)
let base64_secret = "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBNUZranRMRzV5eS9pMFlnYkQxeUJBK21GckNmLzZiQ2F0TDFFQ3ppNG1tZWhSZTcwCkFEL0dSSHhTVUErc0pZeCtZNjlyL0RqQWs2OFJlQ1c4b2FQWXhtc21RNG5VM2ZwZ2E3WWFqZ3ZoWmVsa3JtaC8KZ1ZURWtFTG1IZlJtQkwvOWlsT20yRHNtYTVhUFo0SFl6ellpdjJvcFF5UGRndXcyWXFtbzE3Nk5MdllCMmpJTwovR3FkdE55K3NPV296NktVSVlJa0hWWU5HMENVcFNzdXBqUTJ6VTVZMFc2UXlNQWFWd1BONElJT3lXWUNwZXRECjFJbWxYekhROXM4NXFSWnlLa21iZFhtTVBVWmUvekRxc2FFd3lscFlpT0RjbDdRYU5QTzEzZnk3UGtQMmVwdUkKTk5tZ1E0WEF0MkF4ZXNKck5ibUs4aG1iM3doRXZkNjRFMGdEV1FJREFRQUJBb0lCQUJEemNRd2IyVi8wK1JCMgoyeE5qMll2eHpPTi93S2FYWHBTbUxDUHRIUDhSVEU2RnM0VkZOckdrelBOMmhsL3ZNdjZ4YWdHNk1NbUZ5SFV6CnovSHIyTTY1NjRnOTloaFlXc29FSmFwL3hVYXNjYlhrdWZwZTBZeW4rcThra21JdDRtTmZYRlpXNWI0ODJmNWsKRERVdG5weTVBOEVoSzNOcGw0dnhia0E5dS90TlVlT1NHTkhPYVZjcHdERVhDNXJ4bmFxTm5wMkMwa1A4ODRINgpSb2lZVkF4bytHaVpNVzhIOFRmSXVsenh3c04yQnVNcUNmOGVhNG1EM0pRVHZ2REhhUHM4eVJTUlB3UmlHYUkzCnVybFRmdjg4U20va09oL0N2SkpoRnhCVkVNVjIydWRNUmU3L3NpTWtlbVlvUnhaTWJjRGVQK2h1RktJWTRSMEoKNnRJUHQ3VUNnWUVBOTlhL2IzeFBsQWh0ck02dUlUUXNQd0FYQUg3Q1NXL1FSdVJUTWVhYXVIMk9sRitjZmpMNApJS1Nsdy9QaUtaUEk1TFRWM2ZVZk5WNTVsOFZHTytsT2ViTFhnaXBYM3BqSDBma3AyY3Q2Smk3aGw0aUlXK0h0ClpJNE9KYkYwTTBETHdySkd3T25QL2trRHNxSW9IbC9MdTBRM2FxSm1RVCsvcG54R083R21kbDhDZ1lFQTY5NFcKZHF2NnF4VjF5V0Z4QWZOOE1hZStpTC9xY1VhTm85ZzMva2YvOXZ3VXdtcERvR0xnaVVLMWZKb3BUYlBjcWgwRwptbUZEQ3V2M1Q0OS9yU2k5dU4zYm82cmlXRUl4VFg1YUtFSjlpSEFMWDJGWDdGSDJRdUZGWEwzQ2c0ckdvL1pDCmdjUkxuS3dma3JUVnRxeEdaNjN4YmsvcFpHWjZtTW01VkNDck1VY0NnWUVBc3JUT1pQMG1CSC92VldQU2UyNjcKV05JZncrT2pCSUR6bGFxZHNxV3Rlc3BPUFA2VVFRdFBqM29wYlJvMlFmU21Md09XRXUzbEN2Nk1mcnRvNFZwaAprNjg1WmtwU0FkZjRmWmRFYmg4aWZOWGhKUHIyR0FyWXVtRVVJbW5LZUFxSTRtTGFVZEJHZ2Z6MEJhS1hldzlvClFDZjRMWlBjVjhBMzJUeFRDRWdZMTlFQ2dZQU04U2F5WkVWZzFkQ2N1Q2dIUDJEMUtJc2YzY2Z6WnplbVlkclEKclFxeWRxcDg4Rys5Z1M5bzJLdzBwaERXSHFSaEFTNjNrZGFuNXNLdkx1U0dqOUc1THhNNks4bzNwWW9uQW1QWQpDYTN4cXBRMUs1WXpkVnZaMTVxQ3VEYlFHUEZGVmVIWVZQa0JJOENud0J4cDVaSUhabGYxQVpXQTJNNnBTNGhMCndXOGpTUUtCZ1FDQmNJbjU4Y0lmZkhmMjM4SUJvZnR1UVVzREZGcnkzaUVpaWpTYmJ1WnB1Vm8zL2pWbUsyaEYKS2xUL2xoRDdWdGJ1V3phMG9WQmZDaWZqMnZ2S2pmZ0l6NnF3Um1UbC9DSjlWdUNHTUI1VG55cGl3OEtodXorSAo0L2twdDdNcW9WQ0dRSjd1WVQyQzY1K0JqNklnUnBQT09za3VKNW1RZ0FlbTQ3eDBrVnRSemc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=";
let dec_key = decode_token_rs256_secret_base64(base64_secret).unwrap();
// TOKEN=$(jq -c < json | jwt encode --alg RS256 --secret @./rs256 -)
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJleHAiOjQxMDIzMjQ5ODYsImh0dHBzOi8vand0LmF0dGljLnJzL3YxIjp7ImNhY2hlcyI6eyJhbGwtKiI6eyJyIjoxfSwiYWxsLWNpLSoiOnsidyI6MX0sImNhY2hlLXJvIjp7InIiOjF9LCJjYWNoZS1ydyI6eyJyIjoxLCJ3IjoxfSwidGVhbS0qIjp7ImNjIjoxLCJyIjoxLCJ3IjoxfX19LCJpYXQiOjE3MjIwMDUwNzksIm5iZiI6MCwic3ViIjoibWVvdyJ9.Zs24IUbQOpOjhEe0sfsoSSJhDrzf4v-_wX_ceKqHeb2MERY8XSIQ1RPTNVeOW4LfJHumJj_rxh8Wv2BRGZSMldrTt0Ab_N7FnkhA37_jnRvgvEjSG3V4fC8aA4KoOa-43NRpg4HmPxiXte5-6LneBOR94Wss868wC1b_2yX2zCc1wQoZA3LNo-CRLnL4Yp5wY4Bbgyguv_9mfqXVYZykZnxumyGwVFD-Rub3KQ9d53Rf9tKcvRk9qxO2q8F2PKjeaUBG2xZtGwkWTMvSmwR1dKtkPUyPggOzbLoUG-6fxfo7D3NyL5qWCSN_7CkI-xlsRSLY1gTq-FqXvcpHeZbc8w";
Token::from_jwt(token, &SignatureType::RS256(dec_key), &None, &None).unwrap()
}),
),
];
for (name, decode) in tokens {
eprintln!("Testing {name}");
// NOTE(cole-h): check that we get a consistent iteration order when getting permissions for
// caches -- this depends on the order of the fields in the token, but should otherwise be
// consistent between iterations
let mut was_ever_wrong = false;
for _ in 0..=1_000 {
// NOTE(cole-h): we construct a new Token every iteration in order to get different "random
// state"
let decoded = decode();
let perm_all_ci = decoded.get_permission_for_cache(&cache! { "all-ci-abc" });
// NOTE(cole-h): if the iteration order of the token is inconsistent, the permissions may be
// retrieved from the `all-ci-*` pattern (which only allows writing/pushing), even though
// the `all-*` pattern (which only allows reading/pulling) is specified first
if perm_all_ci.require_pull().is_err() || perm_all_ci.require_push().is_ok() {
was_ever_wrong = true;
}
}
assert!(
!was_ever_wrong,
"Iteration order should be consistent to prevent random auth failures (and successes)"
);
let decoded = decode();
let perm_rw = decoded.get_permission_for_cache(&cache! { "cache-rw" });
assert!(perm_rw.pull);
assert!(perm_rw.push);
assert!(!perm_rw.delete);
assert!(!perm_rw.create_cache);
assert!(perm_rw.require_pull().is_ok());
assert!(perm_rw.require_push().is_ok());
assert!(perm_rw.require_delete().is_err());
assert!(perm_rw.require_create_cache().is_err());
let perm_ro = decoded.get_permission_for_cache(&cache! { "cache-ro" });
assert!(perm_ro.pull);
assert!(!perm_ro.push);
assert!(!perm_ro.delete);
assert!(!perm_ro.create_cache);
assert!(perm_ro.require_pull().is_ok());
assert!(perm_ro.require_push().is_err());
assert!(perm_ro.require_delete().is_err());
assert!(perm_ro.require_create_cache().is_err());
let perm_team = decoded.get_permission_for_cache(&cache! { "team-xyz" });
assert!(perm_team.pull);
assert!(perm_team.push);
assert!(!perm_team.delete);
assert!(perm_team.create_cache);
assert!(perm_team.require_pull().is_ok());
assert!(perm_team.require_push().is_ok());
assert!(perm_team.require_delete().is_err());
assert!(perm_team.require_create_cache().is_ok());
assert!(!decoded
.get_permission_for_cache(&cache! { "forbidden-cache" })
.can_discover());
}
assert!(
!was_ever_wrong,
"Iteration order should be consistent to prevent random auth failures (and successes)"
);
let decoded = Token::from_jwt(token, &dec_key).unwrap();
let perm_rw = decoded.get_permission_for_cache(&cache! { "cache-rw" });
assert!(perm_rw.pull);
assert!(perm_rw.push);
assert!(!perm_rw.delete);
assert!(!perm_rw.create_cache);
assert!(perm_rw.require_pull().is_ok());
assert!(perm_rw.require_push().is_ok());
assert!(perm_rw.require_delete().is_err());
assert!(perm_rw.require_create_cache().is_err());
let perm_ro = decoded.get_permission_for_cache(&cache! { "cache-ro" });
assert!(perm_ro.pull);
assert!(!perm_ro.push);
assert!(!perm_ro.delete);
assert!(!perm_ro.create_cache);
assert!(perm_ro.require_pull().is_ok());
assert!(perm_ro.require_push().is_err());
assert!(perm_ro.require_delete().is_err());
assert!(perm_ro.require_create_cache().is_err());
let perm_team = decoded.get_permission_for_cache(&cache! { "team-xyz" });
assert!(perm_team.pull);
assert!(perm_team.push);
assert!(!perm_team.delete);
assert!(perm_team.create_cache);
assert!(perm_team.require_pull().is_ok());
assert!(perm_team.require_push().is_ok());
assert!(perm_team.require_delete().is_err());
assert!(perm_team.require_create_cache().is_ok());
assert!(!decoded
.get_permission_for_cache(&cache! { "forbidden-cache" })
.can_discover());
}