blob: 98781c296e0bd35a985c356200388aaaeb647b3f [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Personal-access-token request rewriting.
//!
//! The primary mints the raw token plus its hash here, replaces the wire
//! body with the hash-carrying replicated request, and ships only the
//! hash through consensus.
use crate::session_manager::SessionManager;
use crate::wire::{request_body, rewrite_request_body};
use iggy_binary_protocol::requests::personal_access_tokens::{
CreatePersonalAccessTokenRequest as WireCreatePersonalAccessTokenRequest,
DeletePersonalAccessTokenRequest as WireDeletePersonalAccessTokenRequest,
};
use iggy_binary_protocol::{Operation, RequestHeader, WireDecode, WireEncode};
use iggy_common::IggyError;
use metadata::stm::user::{
CreatePersonalAccessTokenRequest as ReplicatedCreatePersonalAccessTokenRequest,
DeletePersonalAccessTokenRequest as ReplicatedDeletePersonalAccessTokenRequest,
};
use server_common::Message;
use std::cell::RefCell;
use std::rc::Rc;
pub(crate) fn maybe_rewrite_pat_request(
sessions: &Rc<RefCell<SessionManager>>,
transport_client_id: u128,
request: Message<RequestHeader>,
) -> Result<(Message<RequestHeader>, Option<String>), IggyError> {
let user_id = match request.header().operation {
Operation::CreatePersonalAccessToken | Operation::DeletePersonalAccessToken => sessions
.borrow()
.get_user_id(transport_client_id)
.ok_or(IggyError::Unauthenticated)?,
_ => return Ok((request, None)),
};
rewrite_pat_request_for_user(user_id, request)
}
/// PAT rewrite for a caller that already holds the authenticated `user_id`.
///
/// The HTTP listener authenticates against its own session table rather than
/// the transport `SessionManager`, so it resolves the id itself and calls this
/// directly. `CreatePersonalAccessToken` mints the raw token + hash and hands
/// the raw token back; `DeletePersonalAccessToken` is rewritten to the
/// replicated form scoped to `user_id` (`only_if_expired` false); every other
/// operation passes through unchanged with `None` (the HTTP write core routes
/// all of its ops here, so the non-PAT arm is a no-op, not `unreachable`).
pub(crate) fn rewrite_pat_request_for_user(
user_id: u32,
request: Message<RequestHeader>,
) -> Result<(Message<RequestHeader>, Option<String>), IggyError> {
let body = request_body(&request);
let mut raw_token = None;
let rewritten = match request.header().operation {
Operation::CreatePersonalAccessToken => {
let wire = WireCreatePersonalAccessTokenRequest::decode_from(body)
.map_err(|_| IggyError::InvalidCommand)?;
// Primary mints the raw token + hash here and ships the hash
// through consensus. Replicas decode the hash directly. Doing
// this inside `CreatePersonalAccessTokenRequest::apply` would
// call `ring::rand` per-replica and diverge state. The raw token
// is returned to this client only (see `handle_client_request`).
let (raw, token_hash) = mint_pat_raw_and_hash();
raw_token = Some(raw);
ReplicatedCreatePersonalAccessTokenRequest {
user_id,
name: wire.name,
expiry: wire.expiry,
token_hash,
}
.to_bytes()
}
Operation::DeletePersonalAccessToken => {
let wire = WireDeletePersonalAccessTokenRequest::decode_from(body)
.map_err(|_| IggyError::InvalidCommand)?;
ReplicatedDeletePersonalAccessTokenRequest {
user_id,
name: wire.name,
// Client-initiated: delete by name unconditionally. Only the
// background cleaner sets the expiry gate.
only_if_expired: false,
}
.to_bytes()
}
_ => return Ok((request, None)),
};
Ok((rewrite_request_body(&request, &rewritten)?, raw_token))
}
/// Mints a fresh PAT and returns the raw token plus its hex-encoded SHA-256
/// hash (64 bytes ASCII). Only the hash is replicated; the raw token is
/// returned to the minting client by the home shard (it cannot be reproduced
/// by the deterministic `apply` running on every replica).
fn mint_pat_raw_and_hash() -> (String, [u8; 64]) {
let (raw, hash) = iggy_common::PersonalAccessToken::mint_raw_and_hash();
let bytes = hash.as_bytes();
// The hash is blake3 hex -- always exactly 64 ASCII chars. `copy_from_slice`
// pins that invariant (panics on a length mismatch) rather than silently
// shipping a wrong-length hash.
let mut out = [0u8; 64];
out.copy_from_slice(bytes);
(raw, out)
}