Skip to content

Commit

Permalink
Add ability to temporarily deny scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
udoprog committed Apr 14, 2024
1 parent d538277 commit 1553c27
Show file tree
Hide file tree
Showing 13 changed files with 125 additions and 50 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions bot/src/auth.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ scopes:
- "@moderator"
auth/permit:
doc: >
If you are allowed to run `!auth permit` to grant temporary scopes.
If you are allowed to run `!auth allow` to grant temporary scopes or `!auth deny` to deny them.
You are only able to grant scopes which you yourself have access to.
version: 0
allow:
Expand Down Expand Up @@ -260,4 +260,4 @@ scopes:
doc: If you are allowed to run the `!weather` command.
version: 0
allow:
- "@everyone"
- "@everyone"
36 changes: 34 additions & 2 deletions bot/src/module/auth.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anyhow::Result;
use async_trait::async_trait;
use auth::TemporaryKind;
use chrono::Utc;
use common::Duration;

Expand Down Expand Up @@ -63,7 +64,7 @@ impl command::Handler for Handler {

ctx.respond_lines(result, "*no scopes*").await;
}
Some("permit") => {
Some("permit") | Some("grant") => {
ctx.check_scope(auth::Scope::AuthPermit).await?;

let duration: Duration = ctx.next_parse("<duration> <principal> <scope>")?;
Expand All @@ -90,7 +91,38 @@ impl command::Handler for Handler {
scope = scope
);

auth.insert_temporary(scope, principal, expires_at).await;
auth.insert_temporary(scope, principal, expires_at, TemporaryKind::Allow)
.await;
}
Some("deny") => {
ctx.check_scope(auth::Scope::AuthPermit).await?;

let duration: Duration = ctx.next_parse("<duration> <principal> <scope>")?;
let principal = ctx.next_parse("<duration> <principal> <scope>")?;
let scope = ctx.next_parse("<duration> <principal> <scope>")?;

if !ctx.user.has_scope(scope).await {
chat::respond!(
ctx,
"Trying to deny scope `{}` that you don't have :(",
scope
);
return Ok(());
}

let now = Utc::now();
let expires_at = now + duration.as_chrono();

chat::respond!(
ctx,
"Denied: {scope} to {principal} for {duration}",
duration = duration,
principal = principal,
scope = scope
);

auth.insert_temporary(scope, principal, expires_at, TemporaryKind::Deny)
.await;
}
_ => {
chat::respond!(ctx, "Expected: scopes, permit");
Expand Down
2 changes: 1 addition & 1 deletion bot/src/sys/windows/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::sys::Notification;

const ICON_MSG_ID: UINT = WM_USER + 1;

thread_local!(static WININFO_STASH: RefCell<Option<WindowsLoopData>> = RefCell::new(None));
thread_local!(static WININFO_STASH: RefCell<Option<WindowsLoopData>> = const { RefCell::new(None) });

/// Copy a wide string from a source to a destination.
pub(crate) fn copy_wstring(dest: &mut [u16], source: &str) {
Expand Down
1 change: 1 addition & 0 deletions crates/oxidize-auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ anyhow = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
chrono = { workspace = true }
tracing = { workspace = true }
90 changes: 60 additions & 30 deletions crates/oxidize-auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,22 @@ impl std::str::FromStr for RoleOrUser {
}
}

/// The kind of temporary grant.
#[derive(Debug, Clone, Copy)]
pub enum TemporaryKind {
Allow,
Deny,
}

/// A grant that has been temporarily given.
struct TemporaryGrant {
struct Temporary {
pub(crate) scope: Scope,
pub(crate) principal: RoleOrUser,
pub(crate) expires_at: DateTime<Utc>,
pub(crate) kind: TemporaryKind,
}

impl TemporaryGrant {
impl Temporary {
/// Test if the grant is expired.
pub(crate) fn is_expired(&self, now: &DateTime<Utc>) -> bool {
*now >= self.expires_at
Expand All @@ -79,7 +87,7 @@ struct Inner {
/// Assignments.
grants: RwLock<HashSet<(Scope, Role)>>,
/// Temporary grants.
temporary_grants: RwLock<Vec<TemporaryGrant>>,
temporary: RwLock<Vec<Temporary>>,
}

/// A container for scopes and their grants.
Expand Down Expand Up @@ -109,7 +117,7 @@ impl Auth {
db,
schema,
grants: RwLock::new(grants),
temporary_grants: Default::default(),
temporary: Default::default(),
}),
};

Expand All @@ -122,7 +130,7 @@ impl Auth {
async fn temporary_scopes(&self, now: &DateTime<Utc>, principal: RoleOrUser) -> Vec<Scope> {
let mut out = Vec::new();

let grants = self.inner.temporary_grants.read().await;
let grants = self.inner.temporary.read().await;

for grant in grants.iter() {
if grant.principal == principal && !grant.is_expired(now) {
Expand Down Expand Up @@ -229,16 +237,24 @@ impl Auth {
scope: Scope,
principal: RoleOrUser,
expires_at: DateTime<Utc>,
kind: TemporaryKind,
) {
self.inner
.temporary_grants
.write()
.await
.push(TemporaryGrant {
let mut grants = self.inner.temporary.write().await;

if let Some(existing) = grants
.iter_mut()
.find(|g| g.scope == scope && g.principal == principal)
{
existing.expires_at = expires_at;
existing.kind = kind;
} else {
grants.push(Temporary {
scope,
principal,
expires_at,
})
kind,
});
}
}

/// Insert an assignment.
Expand Down Expand Up @@ -285,17 +301,17 @@ impl Auth {
now: &DateTime<Utc>,
scope: &Scope,
against: impl IntoIterator<Item = RoleOrUser>,
) -> (bool, bool) {
let temporary = self.inner.temporary_grants.read().await;
) -> (Option<TemporaryKind>, bool) {
let temporary = self.inner.temporary.read().await;

if temporary.is_empty() {
return (false, false);
return (None, false);
}

let mut granted = false;
let mut granted = None;
let mut expired = false;

'outer: for against in against.into_iter() {
'outer: for against in against {
for t in temporary.iter() {
if t.principal != against || t.scope != *scope {
continue;
Expand All @@ -306,7 +322,7 @@ impl Auth {
continue;
}

granted = true;
granted = Some(t.kind);
break 'outer;
}
}
Expand All @@ -325,33 +341,47 @@ impl Auth {
S: AsRef<Scope>,
{
let scope = scope.as_ref();
let roles = roles.into_iter().collect::<HashSet<_>>();

{
let grants = self.inner.grants.read().await;

if roles.iter().any(|r| grants.contains(&(*scope, *r))) {
return true;
}
}
let roles = roles.into_iter().collect::<Vec<_>>();

let now = Utc::now();

let against = iter::once(RoleOrUser::User(user.to_string()))
.chain(roles.into_iter().map(RoleOrUser::Role));
.chain(roles.iter().copied().map(RoleOrUser::Role));

let (granted, expired) = self.test_temporary(&now, scope, against).await;
let (grant, expired) = self.test_temporary(&now, scope, against).await;

// Delete temporary grants that has expired.
if expired {
self.inner
.temporary_grants
.temporary
.write()
.await
.retain(|g| !g.is_expired(&now));
}

granted
let outcome = 'outcome: {
if !matches!(grant, Some(TemporaryKind::Deny)) {
let grants = self.inner.grants.read().await;

if roles.iter().any(|r| grants.contains(&(*scope, *r))) {
break 'outcome true;
}
}

matches!(grant, Some(TemporaryKind::Allow))
};

tracing::info!(
?grant,
?expired,
?scope,
?user,
?roles,
?outcome,
"tested scopes"
);

outcome
}

/// Get a list of scopes and extra information associated with them.
Expand Down
14 changes: 7 additions & 7 deletions crates/oxidize-chat/src/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,16 +628,16 @@ async fn process_command<'a>(
if let Some(handler) = handler {
let scope = handler.scope();

tracing::info! {
?scope,
roles = ?ctx.user.roles(),
principal = ?ctx.user.principal(),
"Testing handler scope"
};

// Test if user has the required scope to run the given
// command.
if let Some(scope) = scope {
tracing::info! {
?scope,
roles = ?ctx.user.roles(),
principal = ?ctx.user.principal(),
"Testing handler scope"
};

if !ctx.user.has_scope(scope).await {
if ctx.user.is_moderator() {
let m = ctx.messages.get(messages::AUTH_FAILED).await;
Expand Down
2 changes: 1 addition & 1 deletion crates/oxidize-chat/src/currency_admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ impl command::Handler for Handler {
if !ctx.user.is_streamer() && ctx.user.is(&boosted_user) {
respond!(
ctx,
"You gonna have to play by the rules (or ask another mod) :("
"You're gonna have to play by the rules (or ask the streamer nicely) :("
);
return Ok(());
}
Expand Down
5 changes: 2 additions & 3 deletions crates/oxidize-currency/src/mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,8 @@ impl Backend {

let balance = self.queries.select_balance(&mut tx, &user).await?;

let balance = match balance {
Some(b) => b.try_into()?,
None => return Ok(None),
let Some(balance) = balance.map(i64::from) else {
return Ok(None);
};

Ok(Some(BalanceOf {
Expand Down
2 changes: 1 addition & 1 deletion crates/oxidize-db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ pub enum RenameError {
Missing,
}

#[cfg(tests)]
#[cfg(test)]
mod tests {
use super::user_id;

Expand Down
2 changes: 1 addition & 1 deletion crates/oxidize-player/src/mixer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ impl Mixer {
queue.push_front(removed);
}

queue.get(0).cloned()
queue.front().cloned()
};

if let Some(item) = next {
Expand Down
13 changes: 11 additions & 2 deletions shared/commands.toml
Original file line number Diff line number Diff line change
Expand Up @@ -914,17 +914,26 @@ SetMod: setbac -> @moderator: song/playback-control, song/spotify, song/list-lim
"""

[[groups.commands]]
name = "!auth permit `<duration>` `<principal>` `<scope>`"
name = "!auth allow `<duration>` `<principal>` `<scope>`"
content = """
Grant a `<scope>` to `<principal>` for `<duration>`.
`<principal>` can either be a role (e.g. _@everyone_ or _@subscriber_) or a user (without _@_).
`<duration>` has to be formatted as `[<days>d][<hours>h][<minutes>m][<seconds>s]`, like _5d10m30s_ or _5m_.
"""

[[groups.commands]]
name = "!auth deny `<duration>` `<principal>` `<scope>`"
content = """
Deny a `<scope>` to `<principal>` for `<duration>`.
`<principal>` can either be a role (e.g. _@everyone_ or _@subscriber_) or a user (without _@_).
`<duration>` has to be formatted as `[<days>d][<hours>h][<minutes>m][<seconds>s]`, like _5d10m30s_ or _5m_.
"""

[[groups.commands.examples]]
name = "Grant _song/spotify_ to _user123_ for _1 minute_"
content = """
setbac: !auth permit 1m user123 song/spotify
SetMod: setbac -> Gave: song/spotify to user123 for 1m
"""
"""
3 changes: 3 additions & 0 deletions web/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
//! <br>
//!
//! The web component of OxidizeBot, a high performance Twitch Bot powered by Rust.

#![allow(clippy::enum_variant_names)]

mod aead;
mod api;
mod db;
Expand Down

0 comments on commit 1553c27

Please sign in to comment.