Add role management commands
This commit is contained in:
parent
301302f1d0
commit
63c2a7fb1d
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "SELECT user_name, user_id, locked FROM users ORDER BY user_name",
|
"query": "SELECT user_name, user_id, locked, COALESCE((\n SELECT array_agg(role_id ORDER BY role_id) FROM users_roles ur WHERE ur.user_id = u.user_id\n ), ARRAY[]::text[]) AS \"roles!\" FROM users u ORDER BY user_name",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@ -17,6 +17,11 @@
|
|||||||
"ordinal": 2,
|
"ordinal": 2,
|
||||||
"name": "locked",
|
"name": "locked",
|
||||||
"type_info": "Bool"
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "roles!",
|
||||||
|
"type_info": "TextArray"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@ -25,8 +30,9 @@
|
|||||||
"nullable": [
|
"nullable": [
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false
|
false,
|
||||||
|
null
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "bbaa41c568bee22eae314186e0d7402f235f8eba9a4ed6adb7bfb6824ec86396"
|
"hash": "21c71fb90265218b7422c756264618e9766dbed4aa10e1a57246fccd8bcedfe1"
|
||||||
}
|
}
|
||||||
32
.sqlx/query-5e059b78f4ebbbd340021e41f2ab64956fa02903597ac65c0ed6b0a527ca02aa.json
generated
Normal file
32
.sqlx/query-5e059b78f4ebbbd340021e41f2ab64956fa02903597ac65c0ed6b0a527ca02aa.json
generated
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT role_id, role_name, (\n SELECT COUNT(1) FROM users_roles ur WHERE ur.role_id = r.role_id\n ) AS \"num_users!\" FROM roles r ORDER BY role_id",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "role_id",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "role_name",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "num_users!",
|
||||||
|
"type_info": "Int8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": []
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "5e059b78f4ebbbd340021e41f2ab64956fa02903597ac65c0ed6b0a527ca02aa"
|
||||||
|
}
|
||||||
15
.sqlx/query-81ca59dcf136d31e129e2d14fbbd6616f6642cdf93e55b85c6e0f7dae58cf229.json
generated
Normal file
15
.sqlx/query-81ca59dcf136d31e129e2d14fbbd6616f6642cdf93e55b85c6e0f7dae58cf229.json
generated
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "INSERT INTO roles (role_id, role_name) VALUES ($1, $2)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text",
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "81ca59dcf136d31e129e2d14fbbd6616f6642cdf93e55b85c6e0f7dae58cf229"
|
||||||
|
}
|
||||||
14
.sqlx/query-8e4a982ac705a58413512a1064eb2d9d3e961fc1d7b707da92737380902fd2b8.json
generated
Normal file
14
.sqlx/query-8e4a982ac705a58413512a1064eb2d9d3e961fc1d7b707da92737380902fd2b8.json
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "DELETE FROM roles WHERE role_id = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "8e4a982ac705a58413512a1064eb2d9d3e961fc1d7b707da92737380902fd2b8"
|
||||||
|
}
|
||||||
15
.sqlx/query-9650a20bd3bb3eee0453d4a8cb2283f41d9b9e84bbd895e1d53369524f611301.json
generated
Normal file
15
.sqlx/query-9650a20bd3bb3eee0453d4a8cb2283f41d9b9e84bbd895e1d53369524f611301.json
generated
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "INSERT INTO users_roles (user_id, role_id, granted_at_utc) VALUES ($1, $2, NOW())",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid",
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "9650a20bd3bb3eee0453d4a8cb2283f41d9b9e84bbd895e1d53369524f611301"
|
||||||
|
}
|
||||||
15
.sqlx/query-d822ddde0ad92c6480e6e10f7bbd7e0317d6f0a41368456032b5f0b5ecb58d7c.json
generated
Normal file
15
.sqlx/query-d822ddde0ad92c6480e6e10f7bbd7e0317d6f0a41368456032b5f0b5ecb58d7c.json
generated
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "DELETE FROM users_roles WHERE user_id = $1 AND role_id = $2",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid",
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "d822ddde0ad92c6480e6e10f7bbd7e0317d6f0a41368456032b5f0b5ecb58d7c"
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@
|
|||||||
- [Command Line Tool](admin/cli/index.md)
|
- [Command Line Tool](admin/cli/index.md)
|
||||||
- [serve](admin/cli/serve.md)
|
- [serve](admin/cli/serve.md)
|
||||||
- [user](admin/cli/user.md)
|
- [user](admin/cli/user.md)
|
||||||
|
- [role](admin/cli/role.md)
|
||||||
|
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|||||||
33
docs/admin/cli/role.md
Normal file
33
docs/admin/cli/role.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# `idcoop role` — role management commands
|
||||||
|
|
||||||
|
## `idcoop role add` — add a new role
|
||||||
|
|
||||||
|
```
|
||||||
|
idcoop role add <ROLE> [--name ROLE_NAME]
|
||||||
|
```
|
||||||
|
aliases: `new`, `create`
|
||||||
|
|
||||||
|
Adds a role.
|
||||||
|
The ROLE identifier must consist of only alphanumeric characters
|
||||||
|
(this may be expanded in the future).
|
||||||
|
|
||||||
|
The optional `--name` flag can be used to give a human-readable name
|
||||||
|
to the role.
|
||||||
|
|
||||||
|
|
||||||
|
## `idcoop role rm` — remove a role
|
||||||
|
|
||||||
|
```
|
||||||
|
idcoop role rm <ROLE>
|
||||||
|
```
|
||||||
|
aliases: `del`, `delete`, `remove`
|
||||||
|
|
||||||
|
Removes a role.
|
||||||
|
|
||||||
|
|
||||||
|
## `idcoop role list` — list all roles
|
||||||
|
|
||||||
|
```
|
||||||
|
idcoop role list
|
||||||
|
```
|
||||||
|
aliases: `ls`
|
||||||
@ -53,15 +53,31 @@ idcoop user <lock|unlock> <USERNAME>
|
|||||||
- `<USERNAME>`: name of the user to be locked or unlocked
|
- `<USERNAME>`: name of the user to be locked or unlocked
|
||||||
|
|
||||||
|
|
||||||
## `idcoop user list-all` — list all users
|
## `idcoop user list` — list all users
|
||||||
|
|
||||||
Displays a list of users in tabular form.
|
Displays a list of users in tabular form.
|
||||||
|
|
||||||
```
|
```
|
||||||
idcoop user list-all [--usernames]
|
idcoop user list [--usernames]
|
||||||
```
|
```
|
||||||
aliases: `idcoop user ls`
|
aliases: `idcoop user ls`
|
||||||
|
|
||||||
- `--usernames`: if specified, only the usernames of users will be shown, one per line.
|
- `--usernames`: if specified, only the usernames of users will be shown, one per line.
|
||||||
|
|
||||||
The output of this command is not considered stable, and should not be used in scripts, unless the `--usernames` option is used.
|
The output of this command is not considered stable, and should not be used in scripts, unless the `--usernames` option is used.
|
||||||
|
|
||||||
|
## `idcoop user role-add` — add users to a role
|
||||||
|
|
||||||
|
The role must exist prior to adding any users to it.
|
||||||
|
|
||||||
|
```
|
||||||
|
idcoop user role-add <ROLE> <USERNAME...>
|
||||||
|
```
|
||||||
|
aliases: `grant`
|
||||||
|
|
||||||
|
## `idcoop user role-rm` — remove users from a role
|
||||||
|
|
||||||
|
```
|
||||||
|
idcoop user role-rm <ROLE> <USERNAME...>
|
||||||
|
```
|
||||||
|
aliases: `revoke`, `role-remove`
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use std::{net::SocketAddr, path::PathBuf};
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use confique::{Config, Partial};
|
use confique::{Config, Partial};
|
||||||
use eyre::{bail, Context};
|
use eyre::{bail, Context};
|
||||||
use idcoop::cli::{handle_user_command, UserCommand};
|
use idcoop::cli::{handle_role_command, handle_user_command, RoleCommand, UserCommand};
|
||||||
use idcoop::config::{SecretConfig, SeparateSecretConfiguration};
|
use idcoop::config::{SecretConfig, SeparateSecretConfiguration};
|
||||||
use idcoop::store::IdCoopStore;
|
use idcoop::store::IdCoopStore;
|
||||||
use idcoop::{config::Configuration, web};
|
use idcoop::{config::Configuration, web};
|
||||||
@ -40,6 +40,12 @@ enum Subcommand {
|
|||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
cmd: UserCommand,
|
cmd: UserCommand,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Manage roles.
|
||||||
|
Role {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
cmd: RoleCommand,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_config_files<C: Config>(files: &[PathBuf]) -> eyre::Result<C> {
|
fn load_config_files<C: Config>(files: &[PathBuf]) -> eyre::Result<C> {
|
||||||
@ -111,6 +117,12 @@ async fn main() -> eyre::Result<()> {
|
|||||||
.context("Failed to connect to Postgres")?;
|
.context("Failed to connect to Postgres")?;
|
||||||
handle_user_command(cmd, &config, &store).await?;
|
handle_user_command(cmd, &config, &store).await?;
|
||||||
}
|
}
|
||||||
|
Subcommand::Role { cmd } => {
|
||||||
|
let store = IdCoopStore::connect(&config.postgres.connect)
|
||||||
|
.await
|
||||||
|
.context("Failed to connect to Postgres")?;
|
||||||
|
handle_role_command(cmd, &config, &store).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
190
src/cli.rs
190
src/cli.rs
@ -1,14 +1,16 @@
|
|||||||
//! idCoop Command Line Interface
|
//! idCoop Command Line Interface
|
||||||
|
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use std::io::stdin;
|
use std::io::stdin;
|
||||||
|
|
||||||
use crate::config::Configuration;
|
use crate::config::Configuration;
|
||||||
use crate::passwords::create_password_hash;
|
use crate::passwords::create_password_hash;
|
||||||
use crate::store::{CreateUser, IdCoopStore};
|
use crate::store::{CreateRole, CreateUser, IdCoopStore, IdCoopStoreTxn};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use comfy_table::presets::UTF8_FULL;
|
use comfy_table::presets::UTF8_FULL;
|
||||||
use comfy_table::{Attribute, Cell, Color, ContentArrangement, Row, Table};
|
use comfy_table::{Attribute, Cell, Color, ContentArrangement, Row, Table};
|
||||||
use eyre::{bail, Context, ContextCompat};
|
use eyre::{bail, Context, ContextCompat};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Commands for user management.
|
/// Commands for user management.
|
||||||
#[derive(Clone, Parser)]
|
#[derive(Clone, Parser)]
|
||||||
@ -49,11 +51,27 @@ pub enum UserCommand {
|
|||||||
},
|
},
|
||||||
/// Lists all users that are registered.
|
/// Lists all users that are registered.
|
||||||
#[clap(alias = "ls")]
|
#[clap(alias = "ls")]
|
||||||
ListAll {
|
List {
|
||||||
/// Only show a list of usernames, without table formatting characters and one per line. May be useful in scripts.
|
/// Only show a list of usernames, without table formatting characters and one per line. May be useful in scripts.
|
||||||
#[clap(long = "usernames")]
|
#[clap(long = "usernames")]
|
||||||
usernames: bool,
|
usernames: bool,
|
||||||
},
|
},
|
||||||
|
/// Adds users to a role.
|
||||||
|
#[clap(alias = "grant")]
|
||||||
|
RoleAdd {
|
||||||
|
/// The ID of the role to add users to.
|
||||||
|
role: String,
|
||||||
|
/// The names of users to add to the role.
|
||||||
|
usernames: Vec<String>,
|
||||||
|
},
|
||||||
|
/// Adds users to a role.
|
||||||
|
#[clap(alias = "role-rm", alias = "revoke")]
|
||||||
|
RoleRemove {
|
||||||
|
/// The ID of the role to remove users from.
|
||||||
|
role: String,
|
||||||
|
/// The names of users to remove from the role.
|
||||||
|
usernames: Vec<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles a user command from the command-line interface.
|
/// Handles a user command from the command-line interface.
|
||||||
@ -143,7 +161,7 @@ pub async fn handle_user_command(
|
|||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
UserCommand::ListAll { usernames } => {
|
UserCommand::List { usernames } => {
|
||||||
let user_infos = store
|
let user_infos = store
|
||||||
.txn(|mut txn| Box::pin(async move { txn.list_user_info().await }))
|
.txn(|mut txn| Box::pin(async move { txn.list_user_info().await }))
|
||||||
.await?;
|
.await?;
|
||||||
@ -157,11 +175,12 @@ pub async fn handle_user_command(
|
|||||||
table
|
table
|
||||||
.load_preset(UTF8_FULL)
|
.load_preset(UTF8_FULL)
|
||||||
.set_content_arrangement(ContentArrangement::Dynamic)
|
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||||
.set_width(80)
|
// .set_width(80)
|
||||||
.set_header(vec![
|
.set_header(vec![
|
||||||
Cell::new("Name").add_attribute(Attribute::Bold),
|
Cell::new("Name").add_attribute(Attribute::Bold),
|
||||||
Cell::new("UUID").add_attribute(Attribute::Bold),
|
Cell::new("UUID").add_attribute(Attribute::Bold),
|
||||||
Cell::new("Locked").add_attribute(Attribute::Bold),
|
Cell::new("Locked").add_attribute(Attribute::Bold),
|
||||||
|
Cell::new("Roles").add_attribute(Attribute::Bold),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
for user_info in user_infos {
|
for user_info in user_infos {
|
||||||
@ -174,12 +193,175 @@ pub async fn handle_user_command(
|
|||||||
("no", Color::White)
|
("no", Color::White)
|
||||||
};
|
};
|
||||||
row.add_cell(Cell::new(lock_str).fg(lock_colour));
|
row.add_cell(Cell::new(lock_str).fg(lock_colour));
|
||||||
|
row.add_cell(Cell::new(user_info.roles.join(", ")));
|
||||||
table.add_row(row);
|
table.add_row(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("{}", table);
|
println!("{}", table);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
UserCommand::RoleAdd { role, usernames } => {
|
||||||
|
let missing_opt = store
|
||||||
|
.txn(|mut txn| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let user_ids = match resolve_usernames(&mut txn, &usernames).await? {
|
||||||
|
Ok(found) => found,
|
||||||
|
Err(missing) => {
|
||||||
|
return Ok(Some(missing));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for user_id in user_ids {
|
||||||
|
txn.add_user_to_role(user_id, &role).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
if let Some(missing) = missing_opt {
|
||||||
|
bail!("Unknown users: {missing:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UserCommand::RoleRemove { role, usernames } => {
|
||||||
|
let missing_opt = store
|
||||||
|
.txn(|mut txn| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let user_ids = match resolve_usernames(&mut txn, &usernames).await? {
|
||||||
|
Ok(found) => found,
|
||||||
|
Err(missing) => {
|
||||||
|
return Ok(Some(missing));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for user_id in user_ids {
|
||||||
|
txn.remove_user_from_role(user_id, &role).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
if let Some(missing) = missing_opt {
|
||||||
|
bail!("Unknown users: {missing:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves usernames to UUIDs, returning them (in the same order).
|
||||||
|
///
|
||||||
|
/// If any of the usernames don't exist, returns a set of the usernames that don't exist instead.
|
||||||
|
async fn resolve_usernames(
|
||||||
|
txn: &mut IdCoopStoreTxn<'_, '_>,
|
||||||
|
usernames: &[String],
|
||||||
|
) -> eyre::Result<Result<Vec<Uuid>, BTreeSet<String>>> {
|
||||||
|
let mut missing = BTreeSet::new();
|
||||||
|
let mut found = Vec::new();
|
||||||
|
|
||||||
|
// Find user IDs
|
||||||
|
for user_name in usernames {
|
||||||
|
match txn.lookup_user_by_name(user_name.clone()).await? {
|
||||||
|
Some(user) => {
|
||||||
|
found.push(user.user_id);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
missing.insert(user_name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !missing.is_empty() {
|
||||||
|
return Ok(Err(missing));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Ok(found))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commands for user management.
|
||||||
|
#[derive(Clone, Parser)]
|
||||||
|
pub enum RoleCommand {
|
||||||
|
/// Add a role.
|
||||||
|
#[clap(alias = "new", alias = "create")]
|
||||||
|
Add {
|
||||||
|
/// The role ID. Must only consist of alphanumeric characters.
|
||||||
|
role: String,
|
||||||
|
|
||||||
|
/// Human-readable name for the role/
|
||||||
|
#[clap(long = "name")]
|
||||||
|
name: Option<String>,
|
||||||
|
},
|
||||||
|
/// Deletes a role.
|
||||||
|
#[clap(alias = "remove", alias = "rm", alias = "del")]
|
||||||
|
Delete {
|
||||||
|
/// The role ID.
|
||||||
|
role: String,
|
||||||
|
},
|
||||||
|
/// List all roles.
|
||||||
|
#[clap(alias = "ls")]
|
||||||
|
List {},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles a role command from the command-line interface.
|
||||||
|
pub async fn handle_role_command(
|
||||||
|
command: RoleCommand,
|
||||||
|
_config: &Configuration,
|
||||||
|
store: &IdCoopStore,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
|
match command {
|
||||||
|
RoleCommand::Add { role, name } => {
|
||||||
|
let name = name.unwrap_or_else(|| role.clone());
|
||||||
|
|
||||||
|
store
|
||||||
|
.txn(|mut txn| {
|
||||||
|
Box::pin(async move {
|
||||||
|
txn.create_role(CreateRole {
|
||||||
|
role_id: role,
|
||||||
|
role_name: name,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
RoleCommand::Delete { role } => {
|
||||||
|
store
|
||||||
|
.txn(|mut txn| Box::pin(async move { txn.delete_role(&role).await }))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
RoleCommand::List {} => {
|
||||||
|
let role_infos = store
|
||||||
|
.txn(|mut txn| Box::pin(async move { txn.list_role_info().await }))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut table = Table::new();
|
||||||
|
table
|
||||||
|
.load_preset(UTF8_FULL)
|
||||||
|
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||||
|
// .set_width(80)
|
||||||
|
.set_header(vec![
|
||||||
|
Cell::new("Role ID").add_attribute(Attribute::Bold),
|
||||||
|
Cell::new("Name").add_attribute(Attribute::Bold),
|
||||||
|
Cell::new("Users").add_attribute(Attribute::Bold),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for role_info in role_infos {
|
||||||
|
let mut row = Row::new();
|
||||||
|
row.add_cell(Cell::new(role_info.role_id));
|
||||||
|
row.add_cell(Cell::new(role_info.role_name).fg(Color::Grey));
|
||||||
|
row.add_cell(
|
||||||
|
Cell::new(role_info.num_users).fg(if role_info.num_users == 0 {
|
||||||
|
Color::Red
|
||||||
|
} else {
|
||||||
|
Color::White
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
table.add_row(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", table);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -110,6 +110,7 @@ pub struct OidcClientConfiguration {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
/// User roles to allow to access this application.
|
/// User roles to allow to access this application.
|
||||||
|
/// Users must satisfy ONE OF the specified roles in order to proceed.
|
||||||
///
|
///
|
||||||
/// The `*` 'role' can be used to allow all users to access this application.
|
/// The `*` 'role' can be used to allow all users to access this application.
|
||||||
///
|
///
|
||||||
|
|||||||
101
src/store.rs
101
src/store.rs
@ -7,6 +7,7 @@ use std::collections::BTreeSet;
|
|||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use eyre::ensure;
|
||||||
use eyre::eyre;
|
use eyre::eyre;
|
||||||
use eyre::Context;
|
use eyre::Context;
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
@ -98,6 +99,14 @@ pub struct CreateUser {
|
|||||||
pub locked: bool,
|
pub locked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Representation of the action of creating a role.
|
||||||
|
pub struct CreateRole {
|
||||||
|
/// Is alphanumeric.
|
||||||
|
pub role_id: String,
|
||||||
|
/// Human-readable name of the role.
|
||||||
|
pub role_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Basic information about a user
|
/// Basic information about a user
|
||||||
pub struct UserInfo {
|
pub struct UserInfo {
|
||||||
/// The unique system name for the user.
|
/// The unique system name for the user.
|
||||||
@ -108,6 +117,21 @@ pub struct UserInfo {
|
|||||||
pub user_id: Uuid,
|
pub user_id: Uuid,
|
||||||
/// Whether the user is locked and is therefore not allowed to log in.
|
/// Whether the user is locked and is therefore not allowed to log in.
|
||||||
pub locked: bool,
|
pub locked: bool,
|
||||||
|
/// List of role IDs the user is in
|
||||||
|
pub roles: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Basic information about a role
|
||||||
|
pub struct RoleInfo {
|
||||||
|
/// The ID of the role.
|
||||||
|
/// Is alphanumeric.
|
||||||
|
pub role_id: String,
|
||||||
|
|
||||||
|
/// The human-readable name of the role.
|
||||||
|
pub role_name: String,
|
||||||
|
|
||||||
|
/// How many users are in the role.
|
||||||
|
pub num_users: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A wrapper around a database transaction with some database methods on it.
|
/// A wrapper around a database transaction with some database methods on it.
|
||||||
@ -298,7 +322,9 @@ impl IdCoopStoreTxn<'_, '_> {
|
|||||||
pub async fn list_user_info(&mut self) -> eyre::Result<Vec<UserInfo>> {
|
pub async fn list_user_info(&mut self) -> eyre::Result<Vec<UserInfo>> {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
UserInfo,
|
UserInfo,
|
||||||
"SELECT user_name, user_id, locked FROM users ORDER BY user_name"
|
r#"SELECT user_name, user_id, locked, COALESCE((
|
||||||
|
SELECT array_agg(role_id ORDER BY role_id) FROM users_roles ur WHERE ur.user_id = u.user_id
|
||||||
|
), ARRAY[]::text[]) AS "roles!" FROM users u ORDER BY user_name"#
|
||||||
)
|
)
|
||||||
.fetch_all(&mut **self.txn)
|
.fetch_all(&mut **self.txn)
|
||||||
.await
|
.await
|
||||||
@ -430,4 +456,77 @@ impl IdCoopStoreTxn<'_, '_> {
|
|||||||
.context("failed to fetch roleset of user")
|
.context("failed to fetch roleset of user")
|
||||||
.map(|v| v.into_iter().collect())
|
.map(|v| v.into_iter().collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a role.
|
||||||
|
pub async fn create_role(&mut self, cr: CreateRole) -> eyre::Result<()> {
|
||||||
|
ensure!(
|
||||||
|
cr.role_id.chars().all(|c| c.is_ascii_alphanumeric()),
|
||||||
|
"attempted to create role {} with non-alphanum ID",
|
||||||
|
cr.role_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO(prettiness): handle the case where the role already exists
|
||||||
|
// in a nicer way
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO roles (role_id, role_name) VALUES ($1, $2)",
|
||||||
|
&cr.role_id,
|
||||||
|
&cr.role_name
|
||||||
|
)
|
||||||
|
.execute(&mut **self.txn)
|
||||||
|
.await
|
||||||
|
.context("failed to create role in DB")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes a role. Silently no-ops if the role doesn't exist.
|
||||||
|
pub async fn delete_role(&mut self, role_id: &str) -> eyre::Result<()> {
|
||||||
|
sqlx::query!("DELETE FROM roles WHERE role_id = $1", role_id)
|
||||||
|
.execute(&mut **self.txn)
|
||||||
|
.await
|
||||||
|
.context("failed to delete role in DB")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lists all roles and provides some information about them.
|
||||||
|
pub async fn list_role_info(&mut self) -> eyre::Result<Vec<RoleInfo>> {
|
||||||
|
sqlx::query_as!(
|
||||||
|
RoleInfo,
|
||||||
|
r#"SELECT role_id, role_name, (
|
||||||
|
SELECT COUNT(1) FROM users_roles ur WHERE ur.role_id = r.role_id
|
||||||
|
) AS "num_users!" FROM roles r ORDER BY role_id"#
|
||||||
|
)
|
||||||
|
.fetch_all(&mut **self.txn)
|
||||||
|
.await
|
||||||
|
.context("failed to list roles")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a user to a role.
|
||||||
|
pub async fn add_user_to_role(&mut self, user_id: Uuid, role_id: &str) -> eyre::Result<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO users_roles (user_id, role_id, granted_at_utc) VALUES ($1, $2, NOW())",
|
||||||
|
user_id,
|
||||||
|
role_id
|
||||||
|
)
|
||||||
|
.execute(&mut **self.txn)
|
||||||
|
.await
|
||||||
|
.context("failed to add user to role")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Removes a user to a role.
|
||||||
|
pub async fn remove_user_from_role(
|
||||||
|
&mut self,
|
||||||
|
user_id: Uuid,
|
||||||
|
role_id: &str,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
"DELETE FROM users_roles WHERE user_id = $1 AND role_id = $2",
|
||||||
|
user_id,
|
||||||
|
role_id
|
||||||
|
)
|
||||||
|
.execute(&mut **self.txn)
|
||||||
|
.await
|
||||||
|
.context("failed to remove user from role")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,8 @@ use crate::{
|
|||||||
|
|
||||||
use super::ext_codes::VolatileCodeStore;
|
use super::ext_codes::VolatileCodeStore;
|
||||||
|
|
||||||
|
/// Role string that always identifies anyone.
|
||||||
|
/// Not a real role.
|
||||||
pub const EVERYONE_ROLE: &str = "*";
|
pub const EVERYONE_ROLE: &str = "*";
|
||||||
|
|
||||||
/// Query string parameters for the OIDC authorisation request.
|
/// Query string parameters for the OIDC authorisation request.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user