parent
771f45baab
commit
2ea14635d2
|
@ -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 `approval_required` and `invites_enabled` flags to `/api/v1/instance` endpoint response.
|
||||||
- Added `registration.type` configuration option (replaces `registrations_open`).
|
- Added `registration.type` configuration option (replaces `registrations_open`).
|
||||||
|
- Implemented roles & permissions.
|
||||||
|
- Added "read-only user" role.
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
|
|
|
@ -114,7 +114,7 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/AccountWithSource'
|
$ref: '#/components/schemas/CredentialAccount'
|
||||||
400:
|
400:
|
||||||
description: Invalid user data
|
description: Invalid user data
|
||||||
/api/v1/accounts/verify_credentials:
|
/api/v1/accounts/verify_credentials:
|
||||||
|
@ -128,7 +128,7 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/AccountWithSource'
|
$ref: '#/components/schemas/CredentialAccount'
|
||||||
/api/v1/accounts/update_credentials:
|
/api/v1/accounts/update_credentials:
|
||||||
patch:
|
patch:
|
||||||
summary: Update the user's display and preferences.
|
summary: Update the user's display and preferences.
|
||||||
|
@ -183,7 +183,7 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/AccountWithSource'
|
$ref: '#/components/schemas/CredentialAccount'
|
||||||
400:
|
400:
|
||||||
description: Invalid user data.
|
description: Invalid user data.
|
||||||
/api/v1/accounts/signed_update:
|
/api/v1/accounts/signed_update:
|
||||||
|
@ -703,7 +703,7 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/AccountWithSource'
|
$ref: '#/components/schemas/CredentialAccount'
|
||||||
400:
|
400:
|
||||||
description: Invalid request data.
|
description: Invalid request data.
|
||||||
/api/v1/settings/export_followers:
|
/api/v1/settings/export_followers:
|
||||||
|
@ -780,7 +780,7 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/AccountWithSource'
|
$ref: '#/components/schemas/CredentialAccount'
|
||||||
400:
|
400:
|
||||||
description: Invalid data.
|
description: Invalid data.
|
||||||
/api/v1/statuses:
|
/api/v1/statuses:
|
||||||
|
@ -1324,7 +1324,7 @@ components:
|
||||||
subscribers_count:
|
subscribers_count:
|
||||||
description: The reported subscribers of this profile.
|
description: The reported subscribers of this profile.
|
||||||
type: number
|
type: number
|
||||||
AccountWithSource:
|
CredentialAccount:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/Account'
|
- $ref: '#/components/schemas/Account'
|
||||||
- type: object
|
- type: object
|
||||||
|
@ -1336,6 +1336,9 @@ components:
|
||||||
note:
|
note:
|
||||||
description: Profile bio.
|
description: Profile bio.
|
||||||
type: string
|
type: string
|
||||||
|
role:
|
||||||
|
description: The role assigned to the currently authorized user.
|
||||||
|
$ref: '#/components/schemas/Role'
|
||||||
ActivityParameters:
|
ActivityParameters:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -1617,6 +1620,28 @@ components:
|
||||||
description: Are you receiving this user's replies in your home timeline?
|
description: Are you receiving this user's replies in your home timeline?
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
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:
|
Signature:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
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),
|
password_hash VARCHAR(200),
|
||||||
private_key TEXT NOT NULL,
|
private_key TEXT NOT NULL,
|
||||||
invite_code VARCHAR(100) UNIQUE REFERENCES user_invite_code (code) ON DELETE SET 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()
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,8 @@ use crate::models::{
|
||||||
subscriptions::types::Subscription,
|
subscriptions::types::Subscription,
|
||||||
users::types::{
|
users::types::{
|
||||||
validate_local_username,
|
validate_local_username,
|
||||||
|
Role,
|
||||||
|
Permission,
|
||||||
User,
|
User,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -53,6 +55,39 @@ pub struct Source {
|
||||||
pub fields: Vec<AccountField>,
|
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/
|
/// https://docs.joinmastodon.org/entities/account/
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
|
@ -74,7 +109,9 @@ pub struct Account {
|
||||||
pub subscribers_count: i32,
|
pub subscribers_count: i32,
|
||||||
pub statuses_count: i32,
|
pub statuses_count: i32,
|
||||||
|
|
||||||
|
// CredentialAccount attributes
|
||||||
pub source: Option<Source>,
|
pub source: Option<Source>,
|
||||||
|
pub role: Option<ApiRole>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Account {
|
impl Account {
|
||||||
|
@ -161,6 +198,7 @@ impl Account {
|
||||||
subscribers_count: profile.subscriber_count,
|
subscribers_count: profile.subscriber_count,
|
||||||
statuses_count: profile.post_count,
|
statuses_count: profile.post_count,
|
||||||
source: None,
|
source: None,
|
||||||
|
role: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,8 +215,10 @@ impl Account {
|
||||||
note: user.profile.bio_source.clone(),
|
note: user.profile.bio_source.clone(),
|
||||||
fields: fields_sources,
|
fields: fields_sources,
|
||||||
};
|
};
|
||||||
|
let role = ApiRole::from_db(user.role);
|
||||||
let mut account = Self::from_profile(user.profile, instance_url);
|
let mut account = Self::from_profile(user.profile, instance_url);
|
||||||
account.source = Some(source);
|
account.source = Some(source);
|
||||||
|
account.role = Some(role);
|
||||||
account
|
account
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ use crate::ipfs::store as ipfs_store;
|
||||||
use crate::ipfs::posts::PostMetadata;
|
use crate::ipfs::posts::PostMetadata;
|
||||||
use crate::ipfs::utils::get_ipfs_url;
|
use crate::ipfs::utils::get_ipfs_url;
|
||||||
use crate::mastodon_api::oauth::auth::get_current_user;
|
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::{
|
use crate::models::posts::queries::{
|
||||||
create_post,
|
create_post,
|
||||||
get_post_by_id,
|
get_post_by_id,
|
||||||
|
@ -68,6 +68,9 @@ async fn create_status(
|
||||||
) -> Result<HttpResponse, HttpError> {
|
) -> Result<HttpResponse, HttpError> {
|
||||||
let db_client = &mut **get_database_client(&db_pool).await?;
|
let db_client = &mut **get_database_client(&db_pool).await?;
|
||||||
let current_user = get_current_user(db_client, auth.token()).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 instance = config.instance();
|
||||||
let status_data = status_data.into_inner();
|
let status_data = status_data.into_inner();
|
||||||
let visibility = match status_data.visibility.as_deref() {
|
let visibility = match status_data.visibility.as_deref() {
|
||||||
|
@ -392,6 +395,9 @@ async fn reblog(
|
||||||
) -> Result<HttpResponse, HttpError> {
|
) -> Result<HttpResponse, HttpError> {
|
||||||
let db_client = &mut **get_database_client(&db_pool).await?;
|
let db_client = &mut **get_database_client(&db_pool).await?;
|
||||||
let current_user = get_current_user(db_client, auth.token()).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?;
|
let mut post = get_post_by_id(db_client, &status_id).await?;
|
||||||
if !post.is_public() || post.repost_of_id.is_some() {
|
if !post.is_public() || post.repost_of_id.is_some() {
|
||||||
return Err(HttpError::NotFoundError("post"));
|
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::reactions::queries::find_favourited_by_user;
|
||||||
use crate::models::relationships::queries::has_relationship;
|
use crate::models::relationships::queries::has_relationship;
|
||||||
use crate::models::relationships::types::RelationshipType;
|
use crate::models::relationships::types::RelationshipType;
|
||||||
use crate::models::users::types::User;
|
use crate::models::users::types::{Permission, User};
|
||||||
use super::queries::{
|
use super::queries::{
|
||||||
get_post_by_id,
|
get_post_by_id,
|
||||||
get_post_by_remote_object_id,
|
get_post_by_remote_object_id,
|
||||||
|
@ -123,6 +123,13 @@ pub async fn can_view_post(
|
||||||
Ok(result)
|
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(
|
pub async fn get_local_post_by_id(
|
||||||
db_client: &impl DatabaseClient,
|
db_client: &impl DatabaseClient,
|
||||||
post_id: &Uuid,
|
post_id: &Uuid,
|
||||||
|
@ -162,7 +169,7 @@ mod tests {
|
||||||
use crate::models::posts::types::PostCreateData;
|
use crate::models::posts::types::PostCreateData;
|
||||||
use crate::models::relationships::queries::{follow, subscribe};
|
use crate::models::relationships::queries::{follow, subscribe};
|
||||||
use crate::models::users::queries::create_user;
|
use crate::models::users::queries::create_user;
|
||||||
use crate::models::users::types::UserCreateData;
|
use crate::models::users::types::{Role, User, UserCreateData};
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -296,4 +303,15 @@ mod tests {
|
||||||
true,
|
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::queries::create_profile;
|
||||||
use crate::models::profiles::types::{DbActorProfile, ProfileCreateData};
|
use crate::models::profiles::types::{DbActorProfile, ProfileCreateData};
|
||||||
use crate::utils::currencies::Currency;
|
use crate::utils::currencies::Currency;
|
||||||
use super::types::{DbUser, User, UserCreateData};
|
use super::types::{DbUser, Role, User, UserCreateData};
|
||||||
use super::utils::generate_invite_code;
|
use super::utils::generate_invite_code;
|
||||||
|
|
||||||
pub async fn create_invite_code(
|
pub async fn create_invite_code(
|
||||||
|
@ -111,9 +111,14 @@ pub async fn create_user(
|
||||||
let row = transaction.query_one(
|
let row = transaction.query_one(
|
||||||
"
|
"
|
||||||
INSERT INTO user_account (
|
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
|
RETURNING user_account
|
||||||
",
|
",
|
||||||
&[
|
&[
|
||||||
|
@ -122,6 +127,7 @@ pub async fn create_user(
|
||||||
&user_data.password_hash,
|
&user_data.password_hash,
|
||||||
&user_data.private_key_pem,
|
&user_data.private_key_pem,
|
||||||
&user_data.invite_code,
|
&user_data.invite_code,
|
||||||
|
&Role::default(),
|
||||||
],
|
],
|
||||||
).await.map_err(catch_unique_violation("user"))?;
|
).await.map_err(catch_unique_violation("user"))?;
|
||||||
let db_user: DbUser = row.try_get("user_account")?;
|
let db_user: DbUser = row.try_get("user_account")?;
|
||||||
|
@ -271,6 +277,19 @@ mod tests {
|
||||||
use crate::database::test_utils::create_test_database;
|
use crate::database::test_utils::create_test_database;
|
||||||
use super::*;
|
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]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn test_create_user_impersonation_protection() {
|
async fn test_create_user_impersonation_protection() {
|
||||||
|
|
|
@ -3,11 +3,81 @@ use postgres_types::FromSql;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::database::{
|
||||||
|
int_enum::{int_enum_from_sql, int_enum_to_sql},
|
||||||
|
DatabaseTypeError,
|
||||||
|
};
|
||||||
use crate::errors::ValidationError;
|
use crate::errors::ValidationError;
|
||||||
use crate::identity::did::Did;
|
use crate::identity::did::Did;
|
||||||
use crate::models::profiles::types::DbActorProfile;
|
use crate::models::profiles::types::DbActorProfile;
|
||||||
use crate::utils::currencies::Currency;
|
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)]
|
#[allow(dead_code)]
|
||||||
#[derive(FromSql)]
|
#[derive(FromSql)]
|
||||||
#[postgres(name = "user_account")]
|
#[postgres(name = "user_account")]
|
||||||
|
@ -17,6 +87,7 @@ pub struct DbUser {
|
||||||
password_hash: Option<String>,
|
password_hash: Option<String>,
|
||||||
private_key: String,
|
private_key: String,
|
||||||
invite_code: Option<String>,
|
invite_code: Option<String>,
|
||||||
|
user_role: Role,
|
||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +99,7 @@ pub struct User {
|
||||||
pub wallet_address: Option<String>, // login address
|
pub wallet_address: Option<String>, // login address
|
||||||
pub password_hash: Option<String>,
|
pub password_hash: Option<String>,
|
||||||
pub private_key: String,
|
pub private_key: String,
|
||||||
|
pub role: Role,
|
||||||
pub profile: DbActorProfile,
|
pub profile: DbActorProfile,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +114,7 @@ impl User {
|
||||||
wallet_address: db_user.wallet_address,
|
wallet_address: db_user.wallet_address,
|
||||||
password_hash: db_user.password_hash,
|
password_hash: db_user.password_hash,
|
||||||
private_key: db_user.private_key,
|
private_key: db_user.private_key,
|
||||||
|
role: db_user.user_role,
|
||||||
profile: db_profile,
|
profile: db_profile,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue