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 API for listing silo users #1261

Merged
merged 22 commits into from
Jun 25, 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
14 changes: 14 additions & 0 deletions nexus/src/app/iam.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use omicron_common::api::external::Error;
use omicron_common::api::external::ListResultVec;
use omicron_common::api::external::LookupResult;
use omicron_common::api::external::UpdateResult;
use uuid::Uuid;

impl super::Nexus {
// Global (fleet-wide) policy
Expand Down Expand Up @@ -54,6 +55,19 @@ impl super::Nexus {
Ok(shared::Policy { role_assignments })
}

// Silo users

pub async fn silo_users_list(
&self,
opctx: &OpContext,
pagparams: &DataPageParams<'_, Uuid>,
) -> ListResultVec<db::model::SiloUser> {
let authz_silo = opctx.authn.silo_required()?;
self.db_datastore
.silo_users_list_by_id(opctx, &authz_silo, pagparams)
.await
}

// Built-in users

pub async fn users_builtin_list(
Expand Down
17 changes: 17 additions & 0 deletions nexus/src/db/datastore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3224,6 +3224,23 @@ impl DataStore {
})
}

pub async fn silo_users_list_by_id(
&self,
opctx: &OpContext,
authz_silo: &authz::Silo,
pagparams: &DataPageParams<'_, Uuid>,
) -> ListResultVec<SiloUser> {
use db::schema::silo_user::dsl;

opctx.authorize(authz::Action::Read, authz_silo).await?;
paginated(dsl::silo_user, dsl::id, pagparams)
.filter(dsl::time_deleted.is_null())
.select(SiloUser::as_select())
.load_async::<SiloUser>(self.pool_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
}

pub async fn users_builtin_list_by_name(
&self,
opctx: &OpContext,
Expand Down
58 changes: 47 additions & 11 deletions nexus/src/external_api/http_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use super::{
console_api, params, views,
views::{
GlobalImage, IdentityProvider, Image, Organization, Project, Rack,
Role, Silo, Sled, Snapshot, SshKey, User, Vpc, VpcRouter, VpcSubnet,
Role, Silo, Sled, Snapshot, SshKey, User, UserBuiltin, Vpc, VpcRouter,
VpcSubnet,
},
};
use crate::authz;
Expand Down Expand Up @@ -199,8 +200,10 @@ pub fn external_api() -> NexusApiDescription {
api.register(sagas_get)?;
api.register(sagas_get_saga)?;

api.register(users_get)?;
api.register(users_get_user)?;
api.register(silo_users_get)?;

api.register(builtin_users_get)?;
api.register(builtin_users_get_user)?;

api.register(timeseries_schema_get)?;

Expand Down Expand Up @@ -3427,18 +3430,51 @@ async fn sagas_get_saga(
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
}

// Silo users

/// List users
#[endpoint {
method = GET,
path = "/users",
tags = ["silos"],
}]
async fn silo_users_get(
rqctx: Arc<RequestContext<Arc<ServerContext>>>,
query_params: Query<PaginatedById>,
) -> Result<HttpResponseOk<ResultsPage<User>>, HttpError> {
let apictx = rqctx.context();
let nexus = &apictx.nexus;
let query = query_params.into_inner();
let pagparams = data_page_params_for(&rqctx, &query)?;
let handler = async {
let opctx = OpContext::for_external_api(&rqctx).await?;
let users = nexus
.silo_users_list(&opctx, &pagparams)
.await?
.into_iter()
.map(|i| i.into())
.collect();
Ok(HttpResponseOk(ScanById::results_page(
&query,
users,
&|_, user: &User| user.id,
)?))
};
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
}

// Built-in (system) users

/// List the built-in system users
#[endpoint {
method = GET,
path = "/users",
tags = ["users"],
path = "/users_builtin",
tags = ["system"],
}]
async fn users_get(
async fn builtin_users_get(
rqctx: Arc<RequestContext<Arc<ServerContext>>>,
query_params: Query<PaginatedByName>,
) -> Result<HttpResponseOk<ResultsPage<User>>, HttpError> {
) -> Result<HttpResponseOk<ResultsPage<UserBuiltin>>, HttpError> {
let apictx = rqctx.context();
let nexus = &apictx.nexus;
let query = query_params.into_inner();
Expand Down Expand Up @@ -3471,13 +3507,13 @@ struct UserPathParam {
/// Fetch a specific built-in system user
#[endpoint {
method = GET,
path = "/users/{user_name}",
tags = ["users"],
path = "/users_builtin/{user_name}",
tags = ["system"],
}]
async fn users_get_user(
async fn builtin_users_get_user(
rqctx: Arc<RequestContext<Arc<ServerContext>>>,
path_params: Path<UserPathParam>,
) -> Result<HttpResponseOk<User>, HttpError> {
) -> Result<HttpResponseOk<UserBuiltin>, HttpError> {
let apictx = rqctx.context();
let nexus = &apictx.nexus;
let path = path_params.into_inner();
Expand Down
8 changes: 4 additions & 4 deletions nexus/src/external_api/tag-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,14 @@
"url": "http://oxide.computer/docs/#xxx"
}
},
"updates": {
"description": "This tag should be moved into a operations tag",
"system": {
"description": "Internal system information",
"external_docs": {
"url": "http://oxide.computer/docs/#xxx"
}
},
"users": {
"description": "This tag should be moved into an IAM tag",
"updates": {
"description": "This tag should be moved into a operations tag",
"external_docs": {
"url": "http://oxide.computer/docs/#xxx"
}
Expand Down
20 changes: 17 additions & 3 deletions nexus/src/external_api/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,18 +408,32 @@ impl From<model::Sled> for Sled {
}
}

// BUILT-IN USERS
// SILO USERS

/// Client view of a [`User`]
#[derive(ObjectIdentity, Clone, Debug, Deserialize, Serialize, JsonSchema)]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, JsonSchema)]
pub struct User {
pub id: Uuid,
}

impl From<model::SiloUser> for User {
fn from(user: model::SiloUser) -> Self {
Self { id: user.id() }
}
}

// BUILT-IN USERS

/// Client view of a [`UserBuiltin`]
#[derive(ObjectIdentity, Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct UserBuiltin {
// TODO-correctness is flattening here (and in all the other types) the
// intent in RFD 4?
#[serde(flatten)]
pub identity: IdentityMetadata,
}

impl From<model::UserBuiltin> for User {
impl From<model::UserBuiltin> for UserBuiltin {
fn from(user: model::UserBuiltin) -> Self {
Self { identity: user.identity() }
}
Expand Down
4 changes: 2 additions & 2 deletions nexus/tests/integration_tests/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ impl AllowedMethod {

lazy_static! {
pub static ref URL_USERS_DB_INIT: String =
format!("/users/{}", authn::USER_DB_INIT.name);
format!("/users_builtin/{}", authn::USER_DB_INIT.name);

/// List of endpoints to be verified
pub static ref VERIFY_ENDPOINTS: Vec<VerifyEndpoint> = vec![
Expand Down Expand Up @@ -999,7 +999,7 @@ lazy_static! {
},

VerifyEndpoint {
url: "/users",
url: "/users_builtin",
visibility: Visibility::Public,
allowed_methods: vec![AllowedMethod::Get],
},
Expand Down
48 changes: 48 additions & 0 deletions nexus/tests/integration_tests/silos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ use nexus_test_utils_macros::nexus_test;
use omicron_nexus::authz::SiloRole;

use httptest::{matchers::*, responders::*, Expectation, Server};
use omicron_nexus::authn::{USER_TEST_PRIVILEGED, USER_TEST_UNPRIVILEGED};
use omicron_nexus::db::fixed_data::silo::SILO_ID;
use omicron_nexus::db::identity::Asset;

#[nexus_test]
async fn test_silos(cptestctx: &ControlPlaneTestContext) {
Expand Down Expand Up @@ -1134,3 +1137,48 @@ async fn test_saml_idp_rsa_keypair_ok(cptestctx: &ControlPlaneTestContext) {
.await
.expect("unexpected failure");
}

#[nexus_test]
async fn test_silo_users_list(cptestctx: &ControlPlaneTestContext) {
let client = &cptestctx.external_client;
let nexus = &cptestctx.server.apictx.nexus;

let initial_silo_users: Vec<views::User> =
NexusRequest::iter_collection_authn(client, "/users", "", None)
.await
.expect("failed to list silo users (1)")
.all_items;

// In the built-in Silo, we expect the test-privileged and test-unprivileged
// users.
assert_eq!(
initial_silo_users,
vec![
views::User { id: USER_TEST_PRIVILEGED.id() },
views::User { id: USER_TEST_UNPRIVILEGED.id() },
]
);

// Now create another user and make sure we can see them. While we're at
// it, use a small limit to check that pagination is really working.
let new_silo_user_id =
"bd75d207-37f3-4769-b808-677ae04eaf23".parse().unwrap();
nexus.silo_user_create(*SILO_ID, new_silo_user_id).await.unwrap();

let silo_users: Vec<views::User> =
NexusRequest::iter_collection_authn(client, "/users", "", Some(1))
.await
.expect("failed to list silo users (2)")
.all_items;
assert_eq!(
silo_users,
vec![
views::User { id: USER_TEST_PRIVILEGED.id() },
views::User { id: USER_TEST_UNPRIVILEGED.id() },
views::User { id: new_silo_user_id },
]
);

// TODO-coverage When we have a way to remove or invalidate Silo Users, we
// should test that doing so causes them to stop appearing in the list.
}
8 changes: 4 additions & 4 deletions nexus/tests/integration_tests/users_builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@ use nexus_test_utils::http_testing::NexusRequest;
use nexus_test_utils::ControlPlaneTestContext;
use nexus_test_utils_macros::nexus_test;
use omicron_nexus::authn;
use omicron_nexus::external_api::views::User;
use omicron_nexus::external_api::views::UserBuiltin;
use std::collections::BTreeMap;

#[nexus_test]
async fn test_users_builtin(cptestctx: &ControlPlaneTestContext) {
let testctx = &cptestctx.external_client;

let mut users = NexusRequest::object_get(&testctx, "/users")
let mut users = NexusRequest::object_get(&testctx, "/users_builtin")
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.unwrap()
.parsed_body::<ResultsPage<User>>()
.parsed_body::<ResultsPage<UserBuiltin>>()
.unwrap()
.items
.into_iter()
.map(|u| (u.identity.name.to_string(), u))
.collect::<BTreeMap<String, User>>();
.collect::<BTreeMap<String, UserBuiltin>>();

let u = users.remove(&authn::USER_DB_INIT.name.to_string()).unwrap();
assert_eq!(u.identity.id, authn::USER_DB_INIT.id);
Expand Down
11 changes: 6 additions & 5 deletions nexus/tests/output/nexus_tags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ API operations found with tag "silos"
OPERATION ID URL PATH
silo_saml_idp_create /silos/{silo_name}/saml_identity_providers
silo_saml_idp_fetch /silos/{silo_name}/saml_identity_providers/{provider_name}
silo_users_get /users
silos_delete_silo /silos/{silo_name}
silos_get /silos
silos_get_identity_providers /silos/{silo_name}/identity_providers
Expand Down Expand Up @@ -166,14 +167,14 @@ vpc_subnets_get_subnet /organizations/{organization_name}/proj
vpc_subnets_post /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets
vpc_subnets_put_subnet /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}

API operations found with tag "updates"
API operations found with tag "system"
OPERATION ID URL PATH
updates_refresh /updates/refresh
builtin_users_get /users_builtin
builtin_users_get_user /users_builtin/{user_name}

API operations found with tag "users"
API operations found with tag "updates"
OPERATION ID URL PATH
users_get /users
users_get_user /users/{user_name}
updates_refresh /updates/refresh

API operations found with tag "vpcs"
OPERATION ID URL PATH
Expand Down
1 change: 1 addition & 0 deletions nexus/tests/output/uncovered-authz-endpoints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ session_me (get "/session/me")
sshkeys_get (get "/session/me/sshkeys")
sshkeys_get_key (get "/session/me/sshkeys/{ssh_key_name}")
silos_get_identity_providers (get "/silos/{silo_name}/identity_providers")
silo_users_get (get "/users")
spoof_login (post "/login")
consume_credentials (post "/login/{silo_name}/{provider_name}")
logout (post "/logout")
Expand Down
Loading