Files
Rocketmc/apps/labrinth/src/models/v3/pats.rs
François-Xavier Talbot 006b19e3c9 Creator tax compliance (#4254)
* Initial implementation

* Remove test code

* Query cache

* Appease clippy

* Precise TIN/SSN

* Make tax threshold customizable via env variable

* Address review comments
2025-08-25 16:34:58 +00:00

259 lines
7.6 KiB
Rust

use crate::bitflags_serde_impl;
use crate::models::ids::PatId;
use ariadne::ids::UserId;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
bitflags::bitflags! {
#[derive(Copy, Clone, Debug)]
pub struct Scopes: u64 {
// read a user's email
const USER_READ_EMAIL = 1 << 0;
// read a user's data
const USER_READ = 1 << 1;
// write to a user's profile (edit username, email, avatar, follows, etc)
const USER_WRITE = 1 << 2;
// delete a user
const USER_DELETE = 1 << 3;
// modify a user's authentication data
const USER_AUTH_WRITE = 1 << 4;
// read a user's notifications
const NOTIFICATION_READ = 1 << 5;
// delete or read a notification
const NOTIFICATION_WRITE = 1 << 6;
// read a user's payouts data
const PAYOUTS_READ = 1 << 7;
// withdraw money from a user's account
const PAYOUTS_WRITE = 1 << 8;
// access user analytics (payout analytics at the moment)
const ANALYTICS = 1 << 9;
// create a project
const PROJECT_CREATE = 1 << 10;
// read a user's projects (including private)
const PROJECT_READ = 1 << 11;
// write to a project's data (metadata, title, team members, etc)
const PROJECT_WRITE = 1 << 12;
// delete a project
const PROJECT_DELETE = 1 << 13;
// create a version
const VERSION_CREATE = 1 << 14;
// read a user's versions (including private)
const VERSION_READ = 1 << 15;
// write to a version's data (metadata, files, etc)
const VERSION_WRITE = 1 << 16;
// delete a version
const VERSION_DELETE = 1 << 17;
// create a report
const REPORT_CREATE = 1 << 18;
// read a user's reports
const REPORT_READ = 1 << 19;
// edit a report
const REPORT_WRITE = 1 << 20;
// delete a report
const REPORT_DELETE = 1 << 21;
// read a thread
const THREAD_READ = 1 << 22;
// write to a thread (send a message, delete a message)
const THREAD_WRITE = 1 << 23;
// create a pat
const PAT_CREATE = 1 << 24;
// read a user's pats
const PAT_READ = 1 << 25;
// edit a pat
const PAT_WRITE = 1 << 26;
// delete a pat
const PAT_DELETE = 1 << 27;
// read a user's sessions
const SESSION_READ = 1 << 28;
// delete a session
const SESSION_DELETE = 1 << 29;
// perform analytics action
const PERFORM_ANALYTICS = 1 << 30;
// create a collection
const COLLECTION_CREATE = 1 << 31;
// read a user's collections
const COLLECTION_READ = 1 << 32;
// write to a collection
const COLLECTION_WRITE = 1 << 33;
// delete a collection
const COLLECTION_DELETE = 1 << 34;
// create an organization
const ORGANIZATION_CREATE = 1 << 35;
// read a user's organizations
const ORGANIZATION_READ = 1 << 36;
// write to an organization
const ORGANIZATION_WRITE = 1 << 37;
// delete an organization
const ORGANIZATION_DELETE = 1 << 38;
// only accessible by modrinth-issued sessions
const SESSION_ACCESS = 1 << 39;
// create a shared instance
const SHARED_INSTANCE_CREATE = 1 << 40;
// read a shared instance
const SHARED_INSTANCE_READ = 1 << 41;
// write to a shared instance
const SHARED_INSTANCE_WRITE = 1 << 42;
// delete a shared instance
const SHARED_INSTANCE_DELETE = 1 << 43;
// create a shared instance version
const SHARED_INSTANCE_VERSION_CREATE = 1 << 44;
// read a shared instance version
const SHARED_INSTANCE_VERSION_READ = 1 << 45;
// write to a shared instance version
const SHARED_INSTANCE_VERSION_WRITE = 1 << 46;
// delete a shared instance version
const SHARED_INSTANCE_VERSION_DELETE = 1 << 47;
const NONE = 0b0;
}
}
bitflags_serde_impl!(Scopes, u64);
impl Scopes {
// these scopes cannot be specified in a personal access token
pub fn restricted() -> Scopes {
Scopes::PAT_CREATE
| Scopes::PAT_READ
| Scopes::PAT_WRITE
| Scopes::PAT_DELETE
| Scopes::SESSION_READ
| Scopes::SESSION_DELETE
| Scopes::SESSION_ACCESS
| Scopes::USER_AUTH_WRITE
| Scopes::USER_DELETE
| Scopes::PERFORM_ANALYTICS
}
pub fn is_restricted(&self) -> bool {
self.intersects(Self::restricted())
}
pub fn parse_from_oauth_scopes(
scopes: &str,
) -> Result<Scopes, bitflags::parser::ParseError> {
let scopes = scopes.replace(['+', ' '], "|").replace("%20", "|");
bitflags::parser::from_str(&scopes)
}
pub fn to_postgres(&self) -> i64 {
self.bits() as i64
}
pub fn from_postgres(value: i64) -> Self {
Self::from_bits(value as u64).unwrap_or(Scopes::NONE)
}
}
#[derive(Serialize, Deserialize)]
pub struct PersonalAccessToken {
pub id: PatId,
pub name: String,
pub access_token: Option<String>,
pub scopes: Scopes,
pub user_id: UserId,
pub created: DateTime<Utc>,
pub expires: DateTime<Utc>,
pub last_used: Option<DateTime<Utc>>,
}
impl PersonalAccessToken {
pub fn from(
data: crate::database::models::pat_item::DBPersonalAccessToken,
include_token: bool,
) -> Self {
Self {
id: data.id.into(),
name: data.name,
access_token: if include_token {
Some(data.access_token)
} else {
None
},
scopes: data.scopes,
user_id: data.user_id.into(),
created: data.created,
expires: data.expires,
last_used: data.last_used,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use itertools::Itertools;
#[test]
fn test_parse_from_oauth_scopes_well_formed() {
let raw = "USER_READ_EMAIL SESSION_READ ORGANIZATION_CREATE";
let expected = Scopes::USER_READ_EMAIL
| Scopes::SESSION_READ
| Scopes::ORGANIZATION_CREATE;
let parsed = Scopes::parse_from_oauth_scopes(raw).unwrap();
assert_same_flags(expected, parsed);
}
#[test]
fn test_parse_from_oauth_scopes_empty() {
let raw = "";
let expected = Scopes::empty();
let parsed = Scopes::parse_from_oauth_scopes(raw).unwrap();
assert_same_flags(expected, parsed);
}
#[test]
fn test_parse_from_oauth_scopes_invalid_scopes() {
let raw = "notascope";
let parsed = Scopes::parse_from_oauth_scopes(raw);
assert!(parsed.is_err());
}
#[test]
fn test_parse_from_oauth_scopes_invalid_separator() {
let raw = "USER_READ_EMAIL & SESSION_READ";
let parsed = Scopes::parse_from_oauth_scopes(raw);
assert!(parsed.is_err());
}
#[test]
fn test_parse_from_oauth_scopes_url_encoded() {
let raw =
urlencoding::encode("PAT_WRITE COLLECTION_DELETE").to_string();
let expected = Scopes::PAT_WRITE | Scopes::COLLECTION_DELETE;
let parsed = Scopes::parse_from_oauth_scopes(&raw).unwrap();
assert_same_flags(expected, parsed);
}
fn assert_same_flags(expected: Scopes, actual: Scopes) {
assert_eq!(
expected.iter_names().map(|(name, _)| name).collect_vec(),
actual.iter_names().map(|(name, _)| name).collect_vec()
);
}
}