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

SSH public keys #954

Merged
merged 17 commits into from
Apr 22, 2022
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
6 changes: 6 additions & 0 deletions common/src/api/external/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ impl From<&Name> for LookupType {
}
}

impl From<Uuid> for LookupType {
fn from(uuid: Uuid) -> Self {
LookupType::ById(uuid)
}
}

impl Error {
/// Returns whether the error is likely transient and could reasonably be
/// retried
Expand Down
1 change: 1 addition & 0 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ pub enum ResourceType {
Fleet,
Silo,
SiloUser,
SshKey,
ConsoleSession,
GlobalImage,
Organization,
Expand Down
27 changes: 27 additions & 0 deletions common/src/sql/dbinit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,33 @@ CREATE TABLE omicron.public.silo_user (
time_deleted TIMESTAMPTZ
);

/*
* Users' public SSH keys, per RFD 44
*/
CREATE TABLE omicron.public.ssh_key (
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,

/* FK into silo_user table */
silo_user_id UUID NOT NULL,

/*
* A 4096 bit RSA key without comment encodes to 726 ASCII characters.
* A (256 bit) Ed25519 key w/o comment encodes to 82 ASCII characters.
*/
public_key STRING(1023) NOT NULL
);

CREATE UNIQUE INDEX ON omicron.public.ssh_key (
silo_user_id,
name
) WHERE
time_deleted IS NULL;

/*
* Organizations
*/
Expand Down
24 changes: 16 additions & 8 deletions nexus/src/authz/api_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,14 +395,6 @@ authz_resource! {
polar_snippet = FleetChild,
}

authz_resource! {
name = "SiloUser",
parent = "Fleet",
primary_key = Uuid,
roles_allowed = false,
polar_snippet = FleetChild,
}

authz_resource! {
name = "RoleBuiltin",
parent = "Fleet",
Expand Down Expand Up @@ -435,6 +427,22 @@ authz_resource! {
polar_snippet = Custom,
}

authz_resource! {
name = "SiloUser",
parent = "Silo",
primary_key = Uuid,
roles_allowed = false,
polar_snippet = Custom,
}

authz_resource! {
name = "SshKey",
parent = "SiloUser",
primary_key = Uuid,
roles_allowed = false,
polar_snippet = Custom,
}

authz_resource! {
name = "Sled",
parent = "Fleet",
Expand Down
29 changes: 28 additions & 1 deletion nexus/src/authz/omicron.polar
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,35 @@ has_relation(fleet: Fleet, "parent_fleet", collection: ConsoleSessionList)
# read silo users and modify their sessions. This is necessary for login to
# work.
has_permission(actor: AuthenticatedActor, "read", user: SiloUser)
if has_role(actor, "external-authenticator", user.fleet);
if has_role(actor, "external-authenticator", user.silo.fleet);
has_permission(actor: AuthenticatedActor, "read", session: ConsoleSession)
if has_role(actor, "external-authenticator", session.fleet);
has_permission(actor: AuthenticatedActor, "modify", session: ConsoleSession)
if has_role(actor, "external-authenticator", session.fleet);

resource SiloUser {
permissions = [
"list_children",
"modify",
"read",
"create_child",
];
relations = { parent_silo: Silo };

"list_children" if "viewer" on "parent_silo";
"read" if "viewer" on "parent_silo";
"modify" if "admin" on "parent_silo";
"create_child" if "admin" on "parent_silo";
}
has_relation(silo: Silo, "parent_silo", user: SiloUser)
if user.silo = silo;

resource SshKey {
permissions = [ "read", "modify" ];
relations = { silo_user: SiloUser };

"read" if "read" on "silo_user";
"modify" if "modify" on "silo_user";
}
has_relation(user: SiloUser, "silo_user", ssh_key: SshKey)
if ssh_key.silo_user = user;
1 change: 1 addition & 0 deletions nexus/src/authz/oso_generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result<Oso, anyhow::Error> {
ConsoleSession::init(),
Rack::init(),
RoleBuiltin::init(),
SshKey::init(),
Silo::init(),
SiloUser::init(),
Sled::init(),
Expand Down
152 changes: 145 additions & 7 deletions nexus/src/db/datastore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ use crate::db::{
InstanceRuntimeState, Name, NetworkInterface, Organization,
OrganizationUpdate, OximeterInfo, ProducerEndpoint, Project,
ProjectUpdate, Region, RoleAssignmentBuiltin, RoleBuiltin, RouterRoute,
RouterRouteUpdate, Silo, SiloUser, Sled, UpdateAvailableArtifact,
UserBuiltin, Volume, Vpc, VpcFirewallRule, VpcRouter, VpcRouterUpdate,
VpcSubnet, VpcSubnetUpdate, VpcUpdate, Zpool,
RouterRouteUpdate, Silo, SiloUser, Sled, SshKey,
UpdateAvailableArtifact, UserBuiltin, Volume, Vpc, VpcFirewallRule,
VpcRouter, VpcRouterUpdate, VpcSubnet, VpcSubnetUpdate, VpcUpdate,
Zpool,
},
pagination::paginated,
pagination::paginated_multicolumn,
Expand Down Expand Up @@ -2769,6 +2770,75 @@ impl DataStore {
)
})
}

// SSH public keys

pub async fn ssh_keys_list(
&self,
opctx: &OpContext,
authz_user: &authz::SiloUser,
page_params: &DataPageParams<'_, Name>,
) -> ListResultVec<SshKey> {
opctx.authorize(authz::Action::ListChildren, authz_user).await?;

davepacheco marked this conversation as resolved.
Show resolved Hide resolved
use db::schema::ssh_key::dsl;
paginated(dsl::ssh_key, dsl::name, page_params)
.filter(dsl::silo_user_id.eq(authz_user.id()))
.filter(dsl::time_deleted.is_null())
.select(SshKey::as_select())
.load_async(self.pool_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
}

/// Create a new SSH public key for a user.
pub async fn ssh_key_create(
&self,
opctx: &OpContext,
authz_user: &authz::SiloUser,
ssh_key: SshKey,
) -> CreateResult<SshKey> {
assert_eq!(authz_user.id(), ssh_key.silo_user_id);
opctx.authorize(authz::Action::CreateChild, authz_user).await?;

davepacheco marked this conversation as resolved.
Show resolved Hide resolved
use db::schema::ssh_key::dsl;
diesel::insert_into(dsl::ssh_key)
.values(ssh_key)
.returning(SshKey::as_returning())
.get_result_async(self.pool_authorized(opctx).await?)
.await
.map_err(|e| {
Error::internal_error(&format!(
"error creating SSH key: {:?}",
e
))
})
}

/// Delete an existing SSH public key.
pub async fn ssh_key_delete(
&self,
opctx: &OpContext,
authz_ssh_key: &authz::SshKey,
) -> DeleteResult {
opctx.authorize(authz::Action::Delete, authz_ssh_key).await?;

use db::schema::ssh_key::dsl;
diesel::update(dsl::ssh_key)
.filter(dsl::id.eq(authz_ssh_key.id()))
.filter(dsl::time_deleted.is_null())
davepacheco marked this conversation as resolved.
Show resolved Hide resolved
.set(dsl::time_deleted.eq(Utc::now()))
.check_if_exists::<SshKey>(authz_ssh_key.id())
.execute_and_check(self.pool_authorized(opctx).await?)
.await
.map_err(|e| {
public_error_from_diesel_pool(
e,
ErrorHandler::NotFoundByResource(authz_ssh_key),
)
})?;
Ok(())
}
}

/// Constructs a DataStore for use in test suites that has preloaded the
Expand Down Expand Up @@ -2890,10 +2960,7 @@ mod test {

// Associate silo with user
let silo_user = datastore
.silo_user_create(SiloUser::new(
Uuid::new_v4(), /* silo id */
silo_user_id,
))
.silo_user_create(SiloUser::new(*SILO_ID, silo_user_id))
.await
.unwrap();

Expand Down Expand Up @@ -3376,4 +3443,75 @@ mod test {
let _ = db.cleanup().await;
logctx.cleanup_successful();
}

#[tokio::test]
async fn test_ssh_keys() {
let logctx = dev::test_setup_log("test_ssh_keys");
let mut db = test_setup_database(&logctx.log).await;
let (opctx, datastore) = datastore_test(&logctx, &db).await;

// Create a new Silo user so that we can lookup their keys.
let silo_user_id = Uuid::new_v4();
let silo_user = datastore
.silo_user_create(SiloUser::new(*SILO_ID, silo_user_id))
.await
.unwrap();
assert_eq!(silo_user.id(), silo_user_id);

let (.., authz_user) = LookupPath::new(&opctx, &datastore)
.silo_user_id(silo_user_id)
.lookup_for(authz::Action::CreateChild)
.await
.unwrap();
assert_eq!(authz_user.id(), silo_user_id);

// Create a new SSH public key for the new user.
let key_name = Name::try_from(String::from("sshkey")).unwrap();
let public_key = "ssh-test AAAAAAAAKEY".to_string();
let ssh_key = SshKey::new(
silo_user_id,
params::SshKeyCreate {
identity: IdentityMetadataCreateParams {
name: key_name.clone(),
description: "my SSH public key".to_string(),
},
public_key,
},
);
let created = datastore
.ssh_key_create(&opctx, &authz_user, ssh_key.clone())
.await
.unwrap();
assert_eq!(created.silo_user_id, ssh_key.silo_user_id);
assert_eq!(created.public_key, ssh_key.public_key);

// Lookup the key we just created.
let (authz_silo, authz_silo_user, authz_ssh_key, found) =
LookupPath::new(&opctx, &datastore)
.silo_user_id(silo_user_id)
.ssh_key_name(&key_name.into())
.fetch()
.await
.unwrap();
assert_eq!(authz_silo.id(), *SILO_ID);
assert_eq!(authz_silo_user.id(), silo_user_id);
assert_eq!(found.silo_user_id, ssh_key.silo_user_id);
assert_eq!(found.public_key, ssh_key.public_key);

// Trying to insert the same one again fails.
let duplicate = datastore
.ssh_key_create(&opctx, &authz_user, ssh_key.clone())
.await;
assert!(matches!(
duplicate,
Err(Error::InternalError { internal_message: _ })
));

// Delete the key we just created.
datastore.ssh_key_delete(&opctx, &authz_ssh_key).await.unwrap();

// Clean up.
db.cleanup().await.unwrap();
logctx.cleanup_successful();
}
}
6 changes: 3 additions & 3 deletions nexus/src/db/db-macros/src/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ fn generate_misc_helpers(config: &Config) -> TokenStream {
if let Err(_) = &maybe_silo {
error!(
log,
"unexpected successful lookup of siloed resource\
davepacheco marked this conversation as resolved.
Show resolved Hide resolved
"unexpected successful lookup of siloed resource \
{:?} with no silo in OpContext",
#resource_name_str,
);
Expand All @@ -352,8 +352,8 @@ fn generate_misc_helpers(config: &Config) -> TokenStream {
use crate::authz::ApiResourceError;
error!(
log,
"unexpected successful lookup of siloed resource\
{:?} in a different Silo from current actor (resource\
"unexpected successful lookup of siloed resource \
{:?} in a different Silo from current actor (resource \
Silo {}, actor Silo {})",
#resource_name_str,
resource_silo_id,
Expand Down
29 changes: 19 additions & 10 deletions nexus/src/db/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ impl<'a> LookupPath<'a> {
Silo { key: SiloKey::PrimaryKey(Root { lookup_root: self }, id) }
}

/// Select a resource of type Silo, identified by its id
/// Select a resource of type Silo, identified by its name
plotnick marked this conversation as resolved.
Show resolved Hide resolved
pub fn silo_name<'b, 'c>(self, name: &'b Name) -> Silo<'c>
where
'a: 'c,
Expand Down Expand Up @@ -403,6 +403,24 @@ lookup_resource! {
primary_key_columns = [ { column_name = "id", rust_type = Uuid } ]
}

lookup_resource! {
name = "SiloUser",
ancestors = [ "Silo" ],
children = [ "SshKey" ],
lookup_by_name = false,
soft_deletes = true,
primary_key_columns = [ { column_name = "id", rust_type = Uuid } ]
}

lookup_resource! {
name = "SshKey",
ancestors = [ "Silo", "SiloUser" ],
children = [],
lookup_by_name = true,
soft_deletes = true,
primary_key_columns = [ { column_name = "id", rust_type = Uuid } ]
}

lookup_resource! {
name = "Organization",
ancestors = [ "Silo" ],
Expand Down Expand Up @@ -509,15 +527,6 @@ lookup_resource! {
]
}

lookup_resource! {
name = "SiloUser",
ancestors = [],
children = [],
lookup_by_name = false,
soft_deletes = true,
primary_key_columns = [ { column_name = "id", rust_type = Uuid } ]
}

lookup_resource! {
name = "Sled",
ancestors = [],
Expand Down
Loading