Created
February 14, 2020 15:04
-
-
Save jcdyer/4c451b925786ba535d1c611cbe1bc8e5 to your computer and use it in GitHub Desktop.
Type-enforced permission system
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use gs1::Gtin; | |
pub mod owned { | |
use gs1::Gtin; | |
// This is constructed as a newtype wrapper around Gtin that only exists | |
// to prove that the GTIN belongs to the currently logged-in user. | |
#[derive(Debug, Clone)] | |
pub struct OwnedGtin(Gtin); | |
impl OwnedGtin { | |
/// Construct an OwnedGtin from a Gtin and an AuthenticatedUser. The | |
/// OwnedGtin can only be returned if the user owns a CompanyPrefix | |
/// that matches the Gtin. | |
/// | |
/// This is the base constructor, which validates the user's claim to | |
/// the GTIN. Most other constructors should delegate to this. | |
pub fn from_gtin(user: &super::auth::AuthenticatedUser, gtin: gs1::Gtin) -> Option<OwnedGtin> { | |
let prefix = user.prefix()?; | |
if format!("{}", gtin)[1..1+prefix.len()] == format!("{}", prefix)[..] { | |
Some(OwnedGtin(gtin)) | |
} else { | |
None | |
} | |
} | |
/// Construct an OwnedGtin from a string and an AuthenticatedUser. | |
pub fn from_str(user: &super::auth::AuthenticatedUser, value: &str) -> Option<OwnedGtin> { | |
let gtin = Gtin::new(String::from(value)).ok()?; | |
OwnedGtin::from_gtin(user, gtin) | |
} | |
/// Construct an OwnedGtin from an AuthenticatedUser, an indicator | |
/// digit, and item reference number. | |
pub fn from_parts(user: &super::auth::AuthenticatedUser, indicator_digit: char, item_ref: u64) -> Option<OwnedGtin> { | |
let prefix = user.prefix()?; | |
let gtin = Gtin::from_parts(prefix, indicator_digit, item_ref).ok()?; | |
OwnedGtin::from_gtin(user, gtin) | |
} | |
/// Get an OwnedGtin that is related to an existing OwnedGtin. | |
/// | |
/// The new OwnedGtin will only differ from the original by the | |
/// indicator digit and the checkdigit. | |
pub fn get_related(&self, indicator_digit: char) -> Option<OwnedGtin> { | |
// This relies on the validation of the original GTIN to ensure the | |
// validation of the new GTIN | |
let new = self.0.get_related(indicator_digit).ok()?; | |
Some(OwnedGtin(new)) | |
} | |
/// Extract the gs1::Gtin from an OwnedGtin. | |
/// | |
/// This consumes the OwnedGtin. | |
pub fn into_inner(self) -> Gtin { | |
self.0 | |
} | |
} | |
} | |
pub mod auth { | |
#[derive(Debug)] | |
pub struct AuthenticatedUser { | |
id: String, | |
prefix: Option<gs1::CompanyPrefix>, | |
} | |
impl AuthenticatedUser { | |
pub fn id(&self) -> &str { | |
&self.id | |
} | |
pub fn prefix(&self) -> Option<&gs1::CompanyPrefix> { | |
self.prefix.as_ref() | |
} | |
} | |
/// Log a user in with a username and password. | |
/// | |
/// This is the only way to get an AuthenticatedUser outside the auth | |
/// module. In a real app, login would have happened in a separate | |
/// context, and we would construct the AuthenticatedUser from an HTTP | |
/// request, and the implementation might check a session variable or | |
/// validate the signature of a JWT. | |
pub fn login(id: &str, password: &str) -> Option<AuthenticatedUser> { | |
match (id, password) { | |
("coke", "is it") => Some(AuthenticatedUser { | |
id: String::from(id), | |
prefix: Some(gs1::CompanyPrefix::new("0123".into()).unwrap()), | |
}), | |
("ipc", "12345") => Some(AuthenticatedUser { | |
id: String::from(id), | |
prefix: Some(gs1::CompanyPrefix::new("10835298".into()).unwrap()), | |
}), | |
("pomnmop", "password") => Some(AuthenticatedUser { | |
id: String::from(id), | |
prefix: None, | |
}), | |
_ => None, | |
} | |
} | |
} | |
#[derive(Debug)] | |
struct Product { | |
name: String, | |
gtin: Gtin, | |
} | |
/// Using a GTIN known to be owned by the current user, create a product. | |
/// | |
/// Note that the product struct holds an unowned GTIN. That struct may be | |
/// accessed by users that don't own the GTIN, but those users should not | |
/// be able to create a new one. | |
/// | |
/// In a complete app, we would use OwnedGtins to protect access to most | |
/// methods that insert or update product records in the database, but GET | |
/// requests would use a plain gs1::Gtin. | |
fn create_product(name: &str, gtin: owned::OwnedGtin) -> Product { | |
Product { | |
name: name.to_owned(), | |
gtin: gtin.into_inner(), | |
} | |
} | |
fn main() { | |
let businessname = "ipc"; | |
let password = "12345"; | |
println!("logging in as {}/{}", businessname, password); | |
let logged_in_user = auth::login(businessname, password).expect("login failed"); | |
println!("{:?}", logged_in_user); | |
print!("Constructing valid owned GTIN: "); | |
let owned_gtin = owned::OwnedGtin::from_str( | |
&logged_in_user, | |
"01083529887658", | |
).expect("Not an owned GTIN"); | |
println!("ok"); | |
print!("Checking that constructing unowned GTIN is not allowed: "); | |
assert!(owned::OwnedGtin::from_str( | |
&logged_in_user, | |
"08283349887658", | |
).is_none()); // Cannot create a gtin that I don't own. | |
println!("ok"); | |
// No failure possible here. The gtin is already checked, and confirmed valid | |
let product = create_product("Multirye bread", owned_gtin); | |
println!("Product: {:?}", product); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment