Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Admin users #414

Merged
merged 12 commits into from
Jan 10, 2024
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
### Added
- Initial support for local predefined Admin users

## [0.151.0] - 2024-01-09
### Added
- New GraphQL APIs to manually schedule and cancel dataset flows
Expand Down
24 changes: 18 additions & 6 deletions resources/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ directive @oneOf on INPUT_OBJECT

type Account {
"""
Unique and stable identitfier of this account
Unique and stable identifier of this account
"""
id: AccountID!
"""
Expand All @@ -28,6 +28,10 @@ type Account {
Avatar URL
"""
avatarUrl: String
"""
Indicates the administrator status
"""
isAdmin: Boolean!
}

scalar AccountDisplayName
Expand Down Expand Up @@ -67,6 +71,10 @@ type AddPushSource {
merge: MergeStrategy!
}

type Admin {
selfTest: String!
}

type AttachmentEmbedded {
path: String!
content: String!
Expand Down Expand Up @@ -529,7 +537,7 @@ type EngineDesc {
name: String!
"""
Language and dialect this engine is using for queries
Indended for configuring code highlighting and completions.
Indented for configuring code highlighting and completions.
"""
dialect: QueryDialect!
"""
Expand Down Expand Up @@ -754,7 +762,7 @@ type Mutation {
"""
Dataset-related functionality group.

Datasets are historical streams of events recorded under a cetrain
Datasets are historical streams of events recorded under a certain
schema.
"""
datasets: DatasetsMut!
Expand Down Expand Up @@ -819,7 +827,7 @@ type Query {
"""
Dataset-related functionality group.

Datasets are historical streams of events recorded under a cetrain
Datasets are historical streams of events recorded under a certain
schema.
"""
datasets: Datasets!
Expand All @@ -839,13 +847,17 @@ type Query {
"""
tasks: Tasks!
"""
Search-related functionality group.
Search-related functionality group
"""
search: Search!
"""
Querying and data manipulations
"""
data: DataQueries!
"""
Admin-related functionality group
"""
admin: Admin!
}

enum QueryDialect {
Expand Down Expand Up @@ -1060,7 +1072,7 @@ type SqlQueryStep {

type Task {
"""
Unique and stable identitfier of this task
Unique and stable identifier of this task
"""
taskId: TaskID!
"""
Expand Down
28 changes: 2 additions & 26 deletions src/adapter/auth-oso/src/kamu_auth_oso.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl KamuAuthOso {
let oso = match KamuAuthOso::load_oso() {
Ok(oso) => oso,
Err(e) => {
panic!("Failed to initialize OSO: {:?}", e);
panic!("Failed to initialize OSO: {e:?}");
}
};

Expand All @@ -40,31 +40,7 @@ impl KamuAuthOso {
oso.register_class(DatasetResource::get_polar_class())?;
oso.register_class(UserActor::get_polar_class())?;

oso.load_str(
r#"
actor UserActor {}

resource DatasetResource {
permissions = ["read", "write"];
}

has_permission(actor: UserActor, "read", dataset: DatasetResource) if
dataset.allows_public_read or
dataset.created_by == actor.name or (
actor_name = actor.name and
dataset.authorized_users.(actor_name) in ["Reader", "Editor"]
);

has_permission(actor: UserActor, "write", dataset: DatasetResource) if
dataset.created_by == actor.name or (
actor_name = actor.name and
dataset.authorized_users.(actor_name) == "Editor"
);

allow(actor: UserActor, action: String, dataset: DatasetResource) if
has_permission(actor, action, dataset);
"#,
)?;
oso.load_str(include_str!("schema.polar"))?;

Ok(oso)
}
Expand Down
6 changes: 4 additions & 2 deletions src/adapter/auth-oso/src/oso_dataset_authorizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ impl OsoDatasetAuthorizer {

fn actor(&self) -> UserActor {
match self.current_account_subject.as_ref() {
CurrentAccountSubject::Anonymous(_) => UserActor::new("", true),
CurrentAccountSubject::Logged(l) => UserActor::new(l.account_name.as_str(), false),
CurrentAccountSubject::Anonymous(_) => UserActor::new("", true, false),
CurrentAccountSubject::Logged(l) => {
UserActor::new(l.account_name.as_str(), false, l.is_admin)
}
}
}

Expand Down
22 changes: 22 additions & 0 deletions src/adapter/auth-oso/src/schema.polar
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
actor UserActor {}

resource DatasetResource {
permissions = ["read", "write"];
}

has_permission(actor: UserActor, "read", dataset: DatasetResource) if
dataset.allows_public_read or
dataset.created_by == actor.name or (
actor_name = actor.name and
dataset.authorized_users.(actor_name) in ["Reader", "Editor"]
);

has_permission(actor: UserActor, "write", dataset: DatasetResource) if
dataset.created_by == actor.name or (
actor_name = actor.name and
dataset.authorized_users.(actor_name) == "Editor"
);

allow(actor: UserActor, action: String, dataset: DatasetResource) if
actor.is_admin or
has_permission(actor, action, dataset);
9 changes: 6 additions & 3 deletions src/adapter/auth-oso/src/user_actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,18 @@ pub struct UserActor {
pub name: String,
#[polar(attribute)]
pub anonymous: bool,
#[polar(attribute)]
pub is_admin: bool,
}

///////////////////////////////////////////////////////////////////////////////

impl UserActor {
pub fn new(name: &str, anonymous: bool) -> Self {
pub fn new(name: &str, anonymous: bool, is_admin: bool) -> Self {
Self {
name: name.to_string(),
anonymous,
is_admin,
}
}
}
Expand All @@ -36,8 +39,8 @@ impl std::fmt::Display for UserActor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"User(name='{}', anonymous={})",
&self.name, self.anonymous
"User(name='{}', anonymous={}, is_admin={})",
&self.name, self.anonymous, self.is_admin
)
}
}
Expand Down
40 changes: 35 additions & 5 deletions src/adapter/auth-oso/tests/tests/test_oso.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ macro_rules! assert_forbidden {

#[test_log::test(tokio::test)]
async fn test_owner_can_read_and_write() {
let user_actor = UserActor::new("foo", false);
let is_admin = false;
let user_actor = UserActor::new("foo", false, is_admin);
let dataset_resource = DatasetResource::new("foo", false);

let oso = KamuAuthOso::new().oso;
Expand All @@ -54,7 +55,8 @@ async fn test_owner_can_read_and_write() {

#[test_log::test(tokio::test)]
async fn test_unrelated_can_read_public() {
let user_actor = UserActor::new("foo", false);
let is_admin = false;
let user_actor = UserActor::new("foo", false, is_admin);
let dataset_resource = DatasetResource::new("bar", true);

let oso = KamuAuthOso::new().oso;
Expand All @@ -78,7 +80,8 @@ async fn test_unrelated_can_read_public() {

#[test_log::test(tokio::test)]
async fn test_unrelated_cannot_read_private() {
let user_actor = UserActor::new("foo", false);
let is_admin = false;
let user_actor = UserActor::new("foo", false, is_admin);
let dataset_resource = DatasetResource::new("bar", false);

let oso = KamuAuthOso::new().oso;
Expand All @@ -102,7 +105,8 @@ async fn test_unrelated_cannot_read_private() {

#[test_log::test(tokio::test)]
async fn test_having_explicit_read_permission_in_private_dataset() {
let user_actor = UserActor::new("foo", false);
let is_admin = false;
let user_actor = UserActor::new("foo", false, is_admin);
let mut dataset_resource = DatasetResource::new("bar", false);
dataset_resource.authorize_reader("foo");

Expand All @@ -127,7 +131,8 @@ async fn test_having_explicit_read_permission_in_private_dataset() {

#[test_log::test(tokio::test)]
async fn test_having_explicit_write_permission_in_private_dataset() {
let user_actor = UserActor::new("foo", false);
let is_admin = false;
let user_actor = UserActor::new("foo", false, is_admin);
let mut dataset_resource = DatasetResource::new("bar", false);
dataset_resource.authorize_editor("foo");

Expand All @@ -149,3 +154,28 @@ async fn test_having_explicit_write_permission_in_private_dataset() {
}

/////////////////////////////////////////////////////////////////////////////////////////

#[test_log::test(tokio::test)]
async fn test_admin_can_read_and_write_another_private_dataset() {
let is_admin = true;
let user_actor = UserActor::new("foo", false, is_admin);
let dataset_resource = DatasetResource::new("bar", false);

let oso = KamuAuthOso::new().oso;

let write_result = oso.is_allowed(
user_actor.clone(),
format!("{}", DatasetAction::Write),
dataset_resource.clone(),
);
let read_result = oso.is_allowed(
user_actor.clone(),
format!("{}", DatasetAction::Read),
dataset_resource.clone(),
);

assert_allowed!(write_result);
assert_allowed!(read_result);
}

/////////////////////////////////////////////////////////////////////////////////////////
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,10 @@ impl DatasetAuthorizerHarness {

let catalog = dill::CatalogBuilder::new()
.add::<EventBus>()
.add_value(CurrentAccountSubject::logged(AccountName::new_unchecked(
current_account_name,
)))
.add_value(CurrentAccountSubject::logged(
AccountName::new_unchecked(current_account_name),
false,
))
.add::<KamuAuthOso>()
.add::<OsoDatasetAuthorizer>()
.add::<DependencyGraphServiceInMemory>()
Expand Down
24 changes: 24 additions & 0 deletions src/adapter/graphql/src/guards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,27 @@ impl Guard for LoggedInGuard {
}

////////////////////////////////////////////////////////////////////////////////////////

pub const STAFF_ONLY_MESSAGE: &str = "Access restricted to administrators only";

pub struct AdminGuard {}

impl AdminGuard {
pub fn new() -> Self {
Self {}
}
}

#[async_trait::async_trait]
impl Guard for AdminGuard {
async fn check(&self, ctx: &Context<'_>) -> Result<()> {
let current_account_subject = from_catalog::<CurrentAccountSubject>(ctx).unwrap();

match current_account_subject.as_ref() {
CurrentAccountSubject::Logged(a) if a.is_admin => Ok(()),
_ => Err(async_graphql::Error::new(STAFF_ONLY_MESSAGE)),
}
}
}

////////////////////////////////////////////////////////////////////////////////////////
36 changes: 23 additions & 13 deletions src/adapter/graphql/src/queries/accounts/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,23 @@ impl Account {
})
}

#[graphql(skip)]
#[inline]
async fn get_full_account_info(
&self,
ctx: &Context<'_>,
) -> Result<&kamu_core::auth::AccountInfo> {
self.full_account_info
.get_or_try_init(|| self.resolve_full_account_info(ctx))
.await
}

#[graphql(skip)]
pub(crate) fn account_name_internal(&self) -> &AccountName {
&self.account_name
}

/// Unique and stable identitfier of this account
/// Unique and stable identifier of this account
async fn id(&self) -> &AccountID {
&self.account_id
}
Expand All @@ -115,10 +126,7 @@ impl Account {

/// Account name to display
async fn display_name(&self, ctx: &Context<'_>) -> Result<AccountDisplayName> {
let full_account_info = self
.full_account_info
.get_or_try_init(|| self.resolve_full_account_info(ctx))
.await?;
let full_account_info = self.get_full_account_info(ctx).await?;

Ok(AccountDisplayName::from(
full_account_info.display_name.clone(),
Expand All @@ -127,10 +135,7 @@ impl Account {

/// Account type
async fn account_type(&self, ctx: &Context<'_>) -> Result<AccountType> {
let full_account_info = self
.full_account_info
.get_or_try_init(|| self.resolve_full_account_info(ctx))
.await?;
let full_account_info = self.get_full_account_info(ctx).await?;

Ok(match full_account_info.account_type {
kamu_core::auth::AccountType::User => AccountType::User,
Expand All @@ -140,12 +145,17 @@ impl Account {

/// Avatar URL
async fn avatar_url(&self, ctx: &Context<'_>) -> Result<&Option<String>> {
let full_account_info = self
.full_account_info
.get_or_try_init(|| self.resolve_full_account_info(ctx))
.await?;
let full_account_info = self.get_full_account_info(ctx).await?;

Ok(&full_account_info.avatar_url)
}

/// Indicates the administrator status
async fn is_admin(&self, ctx: &Context<'_>) -> Result<bool> {
let full_account_info = self.get_full_account_info(ctx).await?;

Ok(full_account_info.is_admin)
}
}

///////////////////////////////////////////////////////////////////////////////
Loading
Loading