parent
771f45baab
commit
2ea14635d2
9 changed files with 198 additions and 12 deletions
|
@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
|
||||
- Added `approval_required` and `invites_enabled` flags to `/api/v1/instance` endpoint response.
|
||||
- Added `registration.type` configuration option (replaces `registrations_open`).
|
||||
- Implemented roles & permissions.
|
||||
- Added "read-only user" role.
|
||||
|
||||
### Deprecated
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AccountWithSource'
|
||||
$ref: '#/components/schemas/CredentialAccount'
|
||||
400:
|
||||
description: Invalid user data
|
||||
/api/v1/accounts/verify_credentials:
|
||||
|
@ -128,7 +128,7 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AccountWithSource'
|
||||
$ref: '#/components/schemas/CredentialAccount'
|
||||
/api/v1/accounts/update_credentials:
|
||||
patch:
|
||||
summary: Update the user's display and preferences.
|
||||
|
@ -183,7 +183,7 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AccountWithSource'
|
||||
$ref: '#/components/schemas/CredentialAccount'
|
||||
400:
|
||||
description: Invalid user data.
|
||||
/api/v1/accounts/signed_update:
|
||||
|
@ -703,7 +703,7 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AccountWithSource'
|
||||
$ref: '#/components/schemas/CredentialAccount'
|
||||
400:
|
||||
description: Invalid request data.
|
||||
/api/v1/settings/export_followers:
|
||||
|
@ -780,7 +780,7 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AccountWithSource'
|
||||
$ref: '#/components/schemas/CredentialAccount'
|
||||
400:
|
||||
description: Invalid data.
|
||||
/api/v1/statuses:
|
||||
|
@ -1324,7 +1324,7 @@ components:
|
|||
subscribers_count:
|
||||
description: The reported subscribers of this profile.
|
||||
type: number
|
||||
AccountWithSource:
|
||||
CredentialAccount:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Account'
|
||||
- type: object
|
||||
|
@ -1336,6 +1336,9 @@ components:
|
|||
note:
|
||||
description: Profile bio.
|
||||
type: string
|
||||
role:
|
||||
description: The role assigned to the currently authorized user.
|
||||
$ref: '#/components/schemas/Role'
|
||||
ActivityParameters:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -1617,6 +1620,28 @@ components:
|
|||
description: Are you receiving this user's replies in your home timeline?
|
||||
type: boolean
|
||||
default: true
|
||||
Role:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: The ID of the role in the database.
|
||||
type: integer
|
||||
example: 1
|
||||
name:
|
||||
description: The name of the role.
|
||||
type: string
|
||||
enum:
|
||||
- user
|
||||
- admin
|
||||
- read_only_user
|
||||
permissions:
|
||||
description: A list of all permissions granted to the role.
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- create_follow_request
|
||||
- create_post
|
||||
Signature:
|
||||
type: object
|
||||
properties:
|
||||
|
|
2
migrations/V0041__user_account__user_role.sql
Normal file
2
migrations/V0041__user_account__user_role.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE user_account ADD COLUMN user_role SMALLINT NOT NULL DEFAULT 1;
|
||||
ALTER TABLE user_account ALTER COLUMN user_role DROP DEFAULT;
|
|
@ -47,6 +47,7 @@ CREATE TABLE user_account (
|
|||
password_hash VARCHAR(200),
|
||||
private_key TEXT NOT NULL,
|
||||
invite_code VARCHAR(100) UNIQUE REFERENCES user_invite_code (code) ON DELETE SET NULL,
|
||||
user_role SMALLINT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ use crate::models::{
|
|||
subscriptions::types::Subscription,
|
||||
users::types::{
|
||||
validate_local_username,
|
||||
Role,
|
||||
Permission,
|
||||
User,
|
||||
},
|
||||
};
|
||||
|
@ -53,6 +55,39 @@ pub struct Source {
|
|||
pub fields: Vec<AccountField>,
|
||||
}
|
||||
|
||||
/// https://docs.joinmastodon.org/entities/Role/
|
||||
#[derive(Serialize)]
|
||||
pub struct ApiRole {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub permissions: Vec<String>,
|
||||
}
|
||||
|
||||
impl ApiRole {
|
||||
fn from_db(role: Role) -> Self {
|
||||
let role_name = match role {
|
||||
Role::Guest => unimplemented!(),
|
||||
Role::NormalUser => "user",
|
||||
Role::Admin => "admin",
|
||||
Role::ReadOnlyUser => "read_only_user",
|
||||
};
|
||||
// Mastodon 4.0 uses bitmask
|
||||
let permissions = role.get_permissions().iter()
|
||||
.map(|permission| {
|
||||
match permission {
|
||||
Permission::CreateFollowRequest => "create_follow_request",
|
||||
Permission::CreatePost => "create_post",
|
||||
}.to_string()
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
id: i16::from(&role).into(),
|
||||
name: role_name.to_string(),
|
||||
permissions: permissions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// https://docs.joinmastodon.org/entities/account/
|
||||
#[derive(Serialize)]
|
||||
pub struct Account {
|
||||
|
@ -74,7 +109,9 @@ pub struct Account {
|
|||
pub subscribers_count: i32,
|
||||
pub statuses_count: i32,
|
||||
|
||||
// CredentialAccount attributes
|
||||
pub source: Option<Source>,
|
||||
pub role: Option<ApiRole>,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
|
@ -161,6 +198,7 @@ impl Account {
|
|||
subscribers_count: profile.subscriber_count,
|
||||
statuses_count: profile.post_count,
|
||||
source: None,
|
||||
role: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -177,8 +215,10 @@ impl Account {
|
|||
note: user.profile.bio_source.clone(),
|
||||
fields: fields_sources,
|
||||
};
|
||||
let role = ApiRole::from_db(user.role);
|
||||
let mut account = Self::from_profile(user.profile, instance_url);
|
||||
account.source = Some(source);
|
||||
account.role = Some(role);
|
||||
account
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ use crate::ipfs::store as ipfs_store;
|
|||
use crate::ipfs::posts::PostMetadata;
|
||||
use crate::ipfs::utils::get_ipfs_url;
|
||||
use crate::mastodon_api::oauth::auth::get_current_user;
|
||||
use crate::models::posts::helpers::can_view_post;
|
||||
use crate::models::posts::helpers::{can_create_post, can_view_post};
|
||||
use crate::models::posts::queries::{
|
||||
create_post,
|
||||
get_post_by_id,
|
||||
|
@ -68,6 +68,9 @@ async fn create_status(
|
|||
) -> Result<HttpResponse, HttpError> {
|
||||
let db_client = &mut **get_database_client(&db_pool).await?;
|
||||
let current_user = get_current_user(db_client, auth.token()).await?;
|
||||
if !can_create_post(¤t_user) {
|
||||
return Err(HttpError::PermissionError);
|
||||
};
|
||||
let instance = config.instance();
|
||||
let status_data = status_data.into_inner();
|
||||
let visibility = match status_data.visibility.as_deref() {
|
||||
|
@ -392,6 +395,9 @@ async fn reblog(
|
|||
) -> Result<HttpResponse, HttpError> {
|
||||
let db_client = &mut **get_database_client(&db_pool).await?;
|
||||
let current_user = get_current_user(db_client, auth.token()).await?;
|
||||
if !can_create_post(¤t_user) {
|
||||
return Err(HttpError::PermissionError);
|
||||
};
|
||||
let mut post = get_post_by_id(db_client, &status_id).await?;
|
||||
if !post.is_public() || post.repost_of_id.is_some() {
|
||||
return Err(HttpError::NotFoundError("post"));
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::database::{DatabaseClient, DatabaseError};
|
|||
use crate::models::reactions::queries::find_favourited_by_user;
|
||||
use crate::models::relationships::queries::has_relationship;
|
||||
use crate::models::relationships::types::RelationshipType;
|
||||
use crate::models::users::types::User;
|
||||
use crate::models::users::types::{Permission, User};
|
||||
use super::queries::{
|
||||
get_post_by_id,
|
||||
get_post_by_remote_object_id,
|
||||
|
@ -123,6 +123,13 @@ pub async fn can_view_post(
|
|||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn can_create_post(
|
||||
user: &User,
|
||||
) -> bool {
|
||||
let permissions = user.role.get_permissions();
|
||||
permissions.contains(&Permission::CreatePost)
|
||||
}
|
||||
|
||||
pub async fn get_local_post_by_id(
|
||||
db_client: &impl DatabaseClient,
|
||||
post_id: &Uuid,
|
||||
|
@ -162,7 +169,7 @@ mod tests {
|
|||
use crate::models::posts::types::PostCreateData;
|
||||
use crate::models::relationships::queries::{follow, subscribe};
|
||||
use crate::models::users::queries::create_user;
|
||||
use crate::models::users::types::UserCreateData;
|
||||
use crate::models::users::types::{Role, User, UserCreateData};
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -296,4 +303,15 @@ mod tests {
|
|||
true,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_can_create_post() {
|
||||
let mut user = User {
|
||||
role: Role::NormalUser,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(can_create_post(&user), true);
|
||||
user.role = Role::ReadOnlyUser;
|
||||
assert_eq!(can_create_post(&user), false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::identity::{did::Did, did_pkh::DidPkh};
|
|||
use crate::models::profiles::queries::create_profile;
|
||||
use crate::models::profiles::types::{DbActorProfile, ProfileCreateData};
|
||||
use crate::utils::currencies::Currency;
|
||||
use super::types::{DbUser, User, UserCreateData};
|
||||
use super::types::{DbUser, Role, User, UserCreateData};
|
||||
use super::utils::generate_invite_code;
|
||||
|
||||
pub async fn create_invite_code(
|
||||
|
@ -111,9 +111,14 @@ pub async fn create_user(
|
|||
let row = transaction.query_one(
|
||||
"
|
||||
INSERT INTO user_account (
|
||||
id, wallet_address, password_hash, private_key, invite_code
|
||||
id,
|
||||
wallet_address,
|
||||
password_hash,
|
||||
private_key,
|
||||
invite_code,
|
||||
user_role
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING user_account
|
||||
",
|
||||
&[
|
||||
|
@ -122,6 +127,7 @@ pub async fn create_user(
|
|||
&user_data.password_hash,
|
||||
&user_data.private_key_pem,
|
||||
&user_data.invite_code,
|
||||
&Role::default(),
|
||||
],
|
||||
).await.map_err(catch_unique_violation("user"))?;
|
||||
let db_user: DbUser = row.try_get("user_account")?;
|
||||
|
@ -271,6 +277,19 @@ mod tests {
|
|||
use crate::database::test_utils::create_test_database;
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_create_user() {
|
||||
let db_client = &mut create_test_database().await;
|
||||
let user_data = UserCreateData {
|
||||
username: "myname".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
let user = create_user(db_client, user_data).await.unwrap();
|
||||
assert_eq!(user.profile.username, "myname");
|
||||
assert_eq!(user.role, Role::NormalUser);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_create_user_impersonation_protection() {
|
||||
|
|
|
@ -3,11 +3,81 @@ use postgres_types::FromSql;
|
|||
use regex::Regex;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::{
|
||||
int_enum::{int_enum_from_sql, int_enum_to_sql},
|
||||
DatabaseTypeError,
|
||||
};
|
||||
use crate::errors::ValidationError;
|
||||
use crate::identity::did::Did;
|
||||
use crate::models::profiles::types::DbActorProfile;
|
||||
use crate::utils::currencies::Currency;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum Permission {
|
||||
CreateFollowRequest,
|
||||
CreatePost,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Role {
|
||||
Guest,
|
||||
NormalUser,
|
||||
Admin,
|
||||
ReadOnlyUser,
|
||||
}
|
||||
|
||||
impl Default for Role {
|
||||
fn default() -> Self { Self::NormalUser }
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn get_permissions(&self) -> Vec<Permission> {
|
||||
match self {
|
||||
Self::Guest => vec![],
|
||||
Self::NormalUser => vec![
|
||||
Permission::CreateFollowRequest,
|
||||
Permission::CreatePost,
|
||||
],
|
||||
Self::Admin => vec![
|
||||
Permission::CreateFollowRequest,
|
||||
Permission::CreatePost,
|
||||
],
|
||||
Self::ReadOnlyUser => vec![
|
||||
Permission::CreateFollowRequest,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Role> for i16 {
|
||||
fn from(value: &Role) -> i16 {
|
||||
match value {
|
||||
Role::Guest => 0,
|
||||
Role::NormalUser => 1,
|
||||
Role::Admin => 2,
|
||||
Role::ReadOnlyUser => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<i16> for Role {
|
||||
type Error = DatabaseTypeError;
|
||||
|
||||
fn try_from(value: i16) -> Result<Self, Self::Error> {
|
||||
let role = match value {
|
||||
0 => Self::Guest,
|
||||
1 => Self::NormalUser,
|
||||
2 => Self::Admin,
|
||||
3 => Self::ReadOnlyUser,
|
||||
_ => return Err(DatabaseTypeError),
|
||||
};
|
||||
Ok(role)
|
||||
}
|
||||
}
|
||||
|
||||
int_enum_from_sql!(Role);
|
||||
int_enum_to_sql!(Role);
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(FromSql)]
|
||||
#[postgres(name = "user_account")]
|
||||
|
@ -17,6 +87,7 @@ pub struct DbUser {
|
|||
password_hash: Option<String>,
|
||||
private_key: String,
|
||||
invite_code: Option<String>,
|
||||
user_role: Role,
|
||||
created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
|
@ -28,6 +99,7 @@ pub struct User {
|
|||
pub wallet_address: Option<String>, // login address
|
||||
pub password_hash: Option<String>,
|
||||
pub private_key: String,
|
||||
pub role: Role,
|
||||
pub profile: DbActorProfile,
|
||||
}
|
||||
|
||||
|
@ -42,6 +114,7 @@ impl User {
|
|||
wallet_address: db_user.wallet_address,
|
||||
password_hash: db_user.password_hash,
|
||||
private_key: db_user.private_key,
|
||||
role: db_user.user_role,
|
||||
profile: db_profile,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue