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

add built-in users to the database #486

Merged
merged 20 commits into from
Dec 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ pub enum ResourceType {
RouterRoute,
Oximeter,
MetricProducer,
User,
Zpool,
}

Expand All @@ -508,6 +509,7 @@ impl Display for ResourceType {
ResourceType::RouterRoute => "vpc router route",
ResourceType::Oximeter => "oximeter",
ResourceType::MetricProducer => "metric producer",
ResourceType::User => "user",
ResourceType::Zpool => "zpool",
}
)
Expand Down
47 changes: 47 additions & 0 deletions common/src/sql/dbinit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,53 @@ CREATE INDEX ON omicron.public.console_session (
time_created
);

/*******************************************************************/

/*
* IAM
*/

/*
* Users built into the system
*
* The ids and names for these users are well-known (i.e., they are used by
* Nexus directly, so changing these would potentially break compatibility).
*/
CREATE TABLE omicron.public.user_builtin (
/*
* Identity metadata
*
* TODO-cleanup This uses the "resource identity" pattern because we want a
* name and description, but it's not valid to support soft-deleting these
* records.
*/
id UUID PRIMARY KEY,
name STRING(63) NOT NULL,
description STRING(512) NOT NULL,
time_created TIMESTAMPTZ NOT NULL,
time_modified TIMESTAMPTZ NOT NULL,
time_deleted TIMESTAMPTZ
);

CREATE UNIQUE INDEX ON omicron.public.user_builtin (name);

/* User used by Nexus to create other users. Do NOT add more users here! */
INSERT INTO omicron.public.user_builtin (
id,
name,
description,
time_created,
time_modified
) VALUES (
/* NOTE: this uuid and name are duplicated in nexus::authn. */
'001de000-05e4-4000-8000-000000000001',
'db-init',
'user used for database initialization',
NOW(),
NOW()
);


/*******************************************************************/

/*
Expand Down
163 changes: 134 additions & 29 deletions nexus/src/authn/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

pub mod external;

use lazy_static::lazy_static;
use omicron_common::api;
use uuid::Uuid;

//
Expand All @@ -34,27 +36,83 @@ use uuid::Uuid;
// Here's a proposed convention for choosing uuids that we hardcode into
// Omicron.
//
// 001de000-05e4-0000-0000-000000000000
// ^^^^^^^^ ^^^^
// +-----|----------------------------- prefix used for all reserved uuids
// | (looks a bit like "oxide")
// +----------------------------- says what kind of resource it is
// ("05e4" looks a bit like "user")
// 001de000-05e4-4000-8000-000000000000
// ^^^^^^^^ ^^^^ ^ ^
// +-----|---|----|-------------------- prefix used for all reserved uuids
// | | | (looks a bit like "oxide")
// +---|----|-------------------- says what kind of resource it is
// | | ("05e4" looks a bit like "user")
// +----|-------------------- v4
// +-------------------- variant 1 (most common for v4)
//
// This way, the uuids stand out a bit. It's not clear if this convention will
// be very useful, but it beats a random uuid.
// be very useful, but it beats a random uuid. (Is it safe to do this? Well,
// these are valid v4 uuids, and they're as unlikely to collide with a future
// uuid as any random uuid is.)
//

/// User id reserved for a test user that's granted many privileges for the
/// purpose of running automated tests.
// "4007" looks a bit like "root".
pub const TEST_USER_UUID_PRIVILEGED: &str =
"001de000-05e4-0000-0000-000000004007";
pub struct UserBuiltinConfig {
pub id: Uuid,
pub name: api::external::Name,
pub description: &'static str,
}

impl UserBuiltinConfig {
fn new_static(
id: &str,
name: &str,
description: &'static str,
) -> UserBuiltinConfig {
UserBuiltinConfig {
id: id.parse().expect("invalid uuid for builtin user id"),
name: name.parse().expect("invalid name for builtin user name"),
description,
}
}
}

/// User id reserved for a test user that has no privileges.
// 60001 is the decimal uid for "nobody" on Helios.
pub const TEST_USER_UUID_UNPRIVILEGED: &str =
"001de000-05e4-0000-0000-000000060001";
lazy_static! {
/// Internal user used for seeding initial database data
// NOTE: This uuid and name are duplicated in dbinit.sql.
pub static ref USER_DB_INIT: UserBuiltinConfig =
UserBuiltinConfig::new_static(
// "0001" is the first possible user that wouldn't be confused with
// 0, or root.
"001de000-05e4-4000-8000-000000000001",
"db-init",
"used for seeding initial database data",
);

/// Internal user used by Nexus when recovering sagas
pub static ref USER_SAGA_RECOVERY: UserBuiltinConfig =
UserBuiltinConfig::new_static(
// "3a8a" looks a bit like "saga".
"001de000-05e4-4000-8000-000000003a8a",
"saga-recovery",
"used by Nexus when recovering sagas",
);

/// Test user that's granted all privileges, used for automated testing and
/// local development
// TODO-security This eventually needs to go, maybe replaced with some kind
// of deployment-specific customization.
pub static ref USER_TEST_PRIVILEGED: UserBuiltinConfig =
UserBuiltinConfig::new_static(
// "4007" looks a bit like "root".
"001de000-05e4-4000-8000-000000004007",
"test-privileged",
"used for testing with all privileges",
);

/// Test user that's granted no privileges, used for automated testing
pub static ref USER_TEST_UNPRIVILEGED: UserBuiltinConfig =
UserBuiltinConfig::new_static(
// 60001 is the decimal uid for "nobody" on Helios.
"001de000-05e4-4000-8000-000000060001",
"test-unprivileged",
"used for testing with no privileges",
);
}

/// Describes how the actor performing the current operation is authenticated
///
Expand Down Expand Up @@ -96,44 +154,91 @@ impl Context {
Context { kind: Kind::Unauthenticated, schemes_tried: vec![] }
}

/// Returns an authenticated context for saga recovery
pub fn internal_saga_recovery() -> Context {
Context::context_for_actor(USER_SAGA_RECOVERY.id)
}

/// Returns an authenticated context for Nexus-startup database
/// initialization
pub fn internal_db_init() -> Context {
Context::context_for_actor(USER_DB_INIT.id)
}

fn context_for_actor(actor_id: Uuid) -> Context {
Context {
kind: Kind::Authenticated(Details { actor: Actor(actor_id) }),
schemes_tried: Vec::new(),
}
}

/// Returns an authenticated context for a special testing user
// TODO-security This eventually needs to go. But for now, this is used
// in unit tests.
#[cfg(test)]
pub fn internal_test_user() -> Context {
Context::test_context_for_actor(
TEST_USER_UUID_PRIVILEGED.parse().unwrap(),
)
Context::test_context_for_actor(USER_TEST_PRIVILEGED.id)
}

/// Returns an authenticated context for a specific user
///
/// This is used for unit testing the authorization rules.
#[cfg(test)]
pub fn test_context_for_actor(actor_id: Uuid) -> Context {
Context {
kind: Kind::Authenticated(Details { actor: Actor(actor_id) }),
schemes_tried: Vec::new(),
}
Context::context_for_actor(actor_id)
}
}

#[cfg(test)]
mod test {
use super::Context;
use super::TEST_USER_UUID_PRIVILEGED;
use super::UserBuiltinConfig;
use super::USER_DB_INIT;
use super::USER_SAGA_RECOVERY;
use super::USER_TEST_PRIVILEGED;
use super::USER_TEST_UNPRIVILEGED;

#[test]
fn test_builtin_ids_are_valid() {
assert_user_has_valid_id(&*USER_DB_INIT);
assert_user_has_valid_id(&*USER_SAGA_RECOVERY);
assert_user_has_valid_id(&*USER_TEST_PRIVILEGED);
assert_user_has_valid_id(&*USER_TEST_UNPRIVILEGED);
}

fn assert_user_has_valid_id(user: &UserBuiltinConfig) {
match user.id.get_version() {
Some(uuid::Version::Random) => (),
_ => panic!("built-in user's uuid is not v4: {:?}", user.name),
};

match user.id.get_variant() {
Some(uuid::Variant::RFC4122) => (),
_ => panic!(
"built-in user's uuid has unexpected variant: {:?}",
user.name
),
};
}

#[test]
fn test_internal_users() {
// The context returned by "internal_unauthenticated()" ought to have no
// associated actor.
let authn = Context::internal_unauthenticated();
assert!(authn.actor().is_none());
// The "internal_test_user()" context ought to refer to the predefined
// test user. This is used in a few places.

// Validate the actor behind various test contexts.
// The privileges are (or will be) verified in authz tests.
let authn = Context::internal_test_user();
let actor = authn.actor().unwrap();
assert_eq!(actor.0.to_string(), TEST_USER_UUID_PRIVILEGED);
assert_eq!(actor.0, USER_TEST_PRIVILEGED.id);

let authn = Context::internal_db_init();
let actor = authn.actor().unwrap();
assert_eq!(actor.0, USER_DB_INIT.id);

let authn = Context::internal_saga_recovery();
let actor = authn.actor().unwrap();
assert_eq!(actor.0, USER_SAGA_RECOVERY.id);
}
}

Expand Down
23 changes: 13 additions & 10 deletions nexus/src/authz/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,31 +103,31 @@ mod test {
use super::Context;
use super::DATABASE;
use super::FLEET;
use crate::authn::TEST_USER_UUID_PRIVILEGED;
use crate::authn::TEST_USER_UUID_UNPRIVILEGED;
use crate::authn;
use std::sync::Arc;

fn authz_context_for_actor(actor_id_str: &str) -> Context {
let actor_id = actor_id_str.parse().expect("bad actor uuid in test");
let authn = crate::authn::Context::test_context_for_actor(actor_id);
fn authz_context_for_actor(authn: authn::Context) -> Context {
let authz = Authz::new();
Context::new(Arc::new(authn), Arc::new(authz))
}

fn authz_context_noauth() -> Context {
let authn = crate::authn::Context::internal_unauthenticated();
let authn = authn::Context::internal_unauthenticated();
let authz = Authz::new();
Context::new(Arc::new(authn), Arc::new(authz))
}

#[test]
fn test_database() {
let authz_privileged =
authz_context_for_actor(TEST_USER_UUID_PRIVILEGED);
authz_context_for_actor(authn::Context::internal_test_user());
authz_privileged
.authorize(Action::Query, DATABASE)
.expect("expected privileged user to be able to query database");
let authz_nobody = authz_context_for_actor(TEST_USER_UUID_UNPRIVILEGED);
let authz_nobody =
authz_context_for_actor(authn::Context::test_context_for_actor(
authn::USER_TEST_UNPRIVILEGED.id,
));
authz_nobody
.authorize(Action::Query, DATABASE)
.expect("expected unprivileged user to be able to query database");
Expand All @@ -140,11 +140,14 @@ mod test {
#[test]
fn test_organization() {
let authz_privileged =
authz_context_for_actor(TEST_USER_UUID_PRIVILEGED);
authz_context_for_actor(authn::Context::internal_test_user());
authz_privileged.authorize(Action::CreateChild, FLEET).expect(
"expected privileged user to be able to create organization",
);
let authz_nobody = authz_context_for_actor(TEST_USER_UUID_UNPRIVILEGED);
let authz_nobody =
authz_context_for_actor(authn::Context::test_context_for_actor(
authn::USER_TEST_UNPRIVILEGED.id,
));
authz_nobody.authorize(Action::CreateChild, FLEET).expect_err(
"expected unprivileged user not to be able to create organization",
);
Expand Down
22 changes: 21 additions & 1 deletion nexus/src/authz/omicron.polar
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ resource Database {
has_role(_actor: AuthenticatedActor, "user", _resource: Database);

#
# Permissions and predefined roles
# Permissions and predefined roles for resources in the
# Fleet/Organization/Project hierarchy
#
# For now, we define the following permissions for most resources in the system:
#
Expand Down Expand Up @@ -178,13 +179,32 @@ resource ProjectChild {
"create_child" if "collaborator" on "parent_project";
}

# Similarly, we use a generic resource to represent every kind of fleet-wide
# resource that's not part of the Organization/Project hierarchy.
resource FleetChild {
permissions = [
"list_children",
"modify",
"read",
"create_child",
];

relations = { parent_fleet: Fleet };
"list_children" if "admin" on "parent_fleet";
"read" if "admin" on "parent_fleet";
"modify" if "admin" on "parent_fleet";
"create_child" if "admin" on "parent_fleet";
}

# Define relationships
has_relation(fleet: Fleet, "parent_fleet", organization: Organization)
if organization.fleet = fleet;
has_relation(organization: Organization, "parent_organization", project: Project)
if project.organization = organization;
has_relation(project: Project, "parent_project", project_child: ProjectChild)
if project_child.project = project;
has_relation(fleet: Fleet, "parent_fleet", fleet_child: FleetChild)
if fleet_child.fleet = fleet;

# Define role relationships
has_role(actor: AuthenticatedActor, role: String, resource: Resource)
Expand Down
Loading