Skip to content

Commit

Permalink
SSH public keys (#954)
Browse files Browse the repository at this point in the history
Add SSH public keys as a new resource type under `SiloUser`.
  • Loading branch information
plotnick authored Apr 22, 2022
1 parent 0592c5f commit d4c11d2
Show file tree
Hide file tree
Showing 23 changed files with 927 additions and 48 deletions.
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?;

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?;

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())
.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\
"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
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

0 comments on commit d4c11d2

Please sign in to comment.