Spiegel von
https://github.com/dani-garcia/vaultwarden.git
synchronisiert 2024-11-22 05:10:29 +01:00
Merge pull request #3568 from BlackDex/org-api-key-refresh
Implement the Organization API Key support for the new Directory Connector v2022
Dieser Commit ist enthalten in:
Commit
5b7d7390b0
19 geänderte Dateien mit 559 neuen und 25 gelöschten Zeilen
|
@ -0,0 +1,10 @@
|
||||||
|
CREATE TABLE organization_api_key (
|
||||||
|
uuid CHAR(36) NOT NULL,
|
||||||
|
org_uuid CHAR(36) NOT NULL REFERENCES organizations(uuid),
|
||||||
|
atype INTEGER NOT NULL,
|
||||||
|
api_key VARCHAR(255) NOT NULL,
|
||||||
|
revision_date DATETIME NOT NULL,
|
||||||
|
PRIMARY KEY(uuid, org_uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE users ADD COLUMN external_id TEXT;
|
|
@ -0,0 +1,10 @@
|
||||||
|
CREATE TABLE organization_api_key (
|
||||||
|
uuid CHAR(36) NOT NULL,
|
||||||
|
org_uuid CHAR(36) NOT NULL REFERENCES organizations(uuid),
|
||||||
|
atype INTEGER NOT NULL,
|
||||||
|
api_key VARCHAR(255),
|
||||||
|
revision_date TIMESTAMP NOT NULL,
|
||||||
|
PRIMARY KEY(uuid, org_uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE users ADD COLUMN external_id TEXT;
|
|
@ -0,0 +1,11 @@
|
||||||
|
CREATE TABLE organization_api_key (
|
||||||
|
uuid TEXT NOT NULL,
|
||||||
|
org_uuid TEXT NOT NULL,
|
||||||
|
atype INTEGER NOT NULL,
|
||||||
|
api_key TEXT NOT NULL,
|
||||||
|
revision_date DATETIME NOT NULL,
|
||||||
|
PRIMARY KEY(uuid, org_uuid),
|
||||||
|
FOREIGN KEY(org_uuid) REFERENCES organizations(uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE users ADD COLUMN external_id TEXT;
|
|
@ -4,6 +4,7 @@ mod emergency_access;
|
||||||
mod events;
|
mod events;
|
||||||
mod folders;
|
mod folders;
|
||||||
mod organizations;
|
mod organizations;
|
||||||
|
mod public;
|
||||||
mod sends;
|
mod sends;
|
||||||
pub mod two_factor;
|
pub mod two_factor;
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
routes.append(&mut organizations::routes());
|
routes.append(&mut organizations::routes());
|
||||||
routes.append(&mut two_factor::routes());
|
routes.append(&mut two_factor::routes());
|
||||||
routes.append(&mut sends::routes());
|
routes.append(&mut sends::routes());
|
||||||
|
routes.append(&mut public::routes());
|
||||||
routes.append(&mut eq_domains_routes);
|
routes.append(&mut eq_domains_routes);
|
||||||
routes.append(&mut hibp_routes);
|
routes.append(&mut hibp_routes);
|
||||||
routes.append(&mut meta_routes);
|
routes.append(&mut meta_routes);
|
||||||
|
|
|
@ -93,7 +93,9 @@ pub fn routes() -> Vec<Route> {
|
||||||
put_reset_password_enrollment,
|
put_reset_password_enrollment,
|
||||||
get_reset_password_details,
|
get_reset_password_details,
|
||||||
put_reset_password,
|
put_reset_password,
|
||||||
get_org_export
|
get_org_export,
|
||||||
|
api_key,
|
||||||
|
rotate_api_key,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2380,7 +2382,7 @@ async fn add_update_group(
|
||||||
"OrganizationId": group.organizations_uuid,
|
"OrganizationId": group.organizations_uuid,
|
||||||
"Name": group.name,
|
"Name": group.name,
|
||||||
"AccessAll": group.access_all,
|
"AccessAll": group.access_all,
|
||||||
"ExternalId": group.get_external_id()
|
"ExternalId": group.external_id
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2891,3 +2893,57 @@ async fn get_org_export(org_id: &str, headers: AdminHeaders, mut conn: DbConn) -
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn _api_key(
|
||||||
|
org_id: &str,
|
||||||
|
data: JsonUpcase<PasswordData>,
|
||||||
|
rotate: bool,
|
||||||
|
headers: AdminHeaders,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
|
let data: PasswordData = data.into_inner().data;
|
||||||
|
let user = headers.user;
|
||||||
|
|
||||||
|
// Validate the admin users password
|
||||||
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
||||||
|
err!("Invalid password")
|
||||||
|
}
|
||||||
|
|
||||||
|
let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_id, &conn).await {
|
||||||
|
Some(mut org_api_key) => {
|
||||||
|
if rotate {
|
||||||
|
org_api_key.api_key = crate::crypto::generate_api_key();
|
||||||
|
org_api_key.revision_date = chrono::Utc::now().naive_utc();
|
||||||
|
org_api_key.save(&conn).await.expect("Error rotating organization API Key");
|
||||||
|
}
|
||||||
|
org_api_key
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let api_key = crate::crypto::generate_api_key();
|
||||||
|
let new_org_api_key = OrganizationApiKey::new(String::from(org_id), api_key);
|
||||||
|
new_org_api_key.save(&conn).await.expect("Error creating organization API Key");
|
||||||
|
new_org_api_key
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"ApiKey": org_api_key.api_key,
|
||||||
|
"RevisionDate": crate::util::format_date(&org_api_key.revision_date),
|
||||||
|
"Object": "apiKey",
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/organizations/<org_id>/api-key", data = "<data>")]
|
||||||
|
async fn api_key(org_id: &str, data: JsonUpcase<PasswordData>, headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
||||||
|
_api_key(org_id, data, false, headers, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/organizations/<org_id>/rotate-api-key", data = "<data>")]
|
||||||
|
async fn rotate_api_key(
|
||||||
|
org_id: &str,
|
||||||
|
data: JsonUpcase<PasswordData>,
|
||||||
|
headers: AdminHeaders,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
|
_api_key(org_id, data, true, headers, conn).await
|
||||||
|
}
|
||||||
|
|
238
src/api/core/public.rs
Normale Datei
238
src/api/core/public.rs
Normale Datei
|
@ -0,0 +1,238 @@
|
||||||
|
use chrono::Utc;
|
||||||
|
use rocket::{
|
||||||
|
request::{self, FromRequest, Outcome},
|
||||||
|
Request, Route,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api::{EmptyResult, JsonUpcase},
|
||||||
|
auth,
|
||||||
|
db::{models::*, DbConn},
|
||||||
|
mail, CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn routes() -> Vec<Route> {
|
||||||
|
routes![ldap_import]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct OrgImportGroupData {
|
||||||
|
Name: String,
|
||||||
|
ExternalId: String,
|
||||||
|
MemberExternalIds: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct OrgImportUserData {
|
||||||
|
Email: String,
|
||||||
|
ExternalId: String,
|
||||||
|
Deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct OrgImportData {
|
||||||
|
Groups: Vec<OrgImportGroupData>,
|
||||||
|
Members: Vec<OrgImportUserData>,
|
||||||
|
OverwriteExisting: bool,
|
||||||
|
// LargeImport: bool, // For now this will not be used, upstream uses this to prevent syncs of more then 2000 users or groups without the flag set.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/public/organization/import", data = "<data>")]
|
||||||
|
async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut conn: DbConn) -> EmptyResult {
|
||||||
|
// Most of the logic for this function can be found here
|
||||||
|
// https://github.com/bitwarden/server/blob/fd892b2ff4547648a276734fb2b14a8abae2c6f5/src/Core/Services/Implementations/OrganizationService.cs#L1797
|
||||||
|
|
||||||
|
let org_id = token.0;
|
||||||
|
let data = data.into_inner().data;
|
||||||
|
|
||||||
|
for user_data in &data.Members {
|
||||||
|
if user_data.Deleted {
|
||||||
|
// If user is marked for deletion and it exists, revoke it
|
||||||
|
if let Some(mut user_org) =
|
||||||
|
UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await
|
||||||
|
{
|
||||||
|
user_org.revoke();
|
||||||
|
user_org.save(&mut conn).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user is part of the organization, restore it
|
||||||
|
} else if let Some(mut user_org) =
|
||||||
|
UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await
|
||||||
|
{
|
||||||
|
if user_org.status < UserOrgStatus::Revoked as i32 {
|
||||||
|
user_org.restore();
|
||||||
|
user_org.save(&mut conn).await?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If user is not part of the organization
|
||||||
|
let user = match User::find_by_mail(&user_data.Email, &mut conn).await {
|
||||||
|
Some(user) => user, // exists in vaultwarden
|
||||||
|
None => {
|
||||||
|
// doesn't exist in vaultwarden
|
||||||
|
let mut new_user = User::new(user_data.Email.clone());
|
||||||
|
new_user.set_external_id(Some(user_data.ExternalId.clone()));
|
||||||
|
new_user.save(&mut conn).await?;
|
||||||
|
|
||||||
|
if !CONFIG.mail_enabled() {
|
||||||
|
let invitation = Invitation::new(&new_user.email);
|
||||||
|
invitation.save(&mut conn).await?;
|
||||||
|
}
|
||||||
|
new_user
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let user_org_status = if CONFIG.mail_enabled() {
|
||||||
|
UserOrgStatus::Invited as i32
|
||||||
|
} else {
|
||||||
|
UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut new_org_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
|
||||||
|
new_org_user.access_all = false;
|
||||||
|
new_org_user.atype = UserOrgType::User as i32;
|
||||||
|
new_org_user.status = user_org_status;
|
||||||
|
|
||||||
|
new_org_user.save(&mut conn).await?;
|
||||||
|
|
||||||
|
if CONFIG.mail_enabled() {
|
||||||
|
let (org_name, org_email) = match Organization::find_by_uuid(&org_id, &mut conn).await {
|
||||||
|
Some(org) => (org.name, org.billing_email),
|
||||||
|
None => err!("Error looking up organization"),
|
||||||
|
};
|
||||||
|
|
||||||
|
mail::send_invite(
|
||||||
|
&user_data.Email,
|
||||||
|
&user.uuid,
|
||||||
|
Some(org_id.clone()),
|
||||||
|
Some(new_org_user.uuid),
|
||||||
|
&org_name,
|
||||||
|
Some(org_email),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if CONFIG.org_groups_enabled() {
|
||||||
|
for group_data in &data.Groups {
|
||||||
|
let group_uuid = match Group::find_by_external_id(&group_data.ExternalId, &mut conn).await {
|
||||||
|
Some(group) => group.uuid,
|
||||||
|
None => {
|
||||||
|
let mut group =
|
||||||
|
Group::new(org_id.clone(), group_data.Name.clone(), false, Some(group_data.ExternalId.clone()));
|
||||||
|
group.save(&mut conn).await?;
|
||||||
|
group.uuid
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
GroupUser::delete_all_by_group(&group_uuid, &mut conn).await?;
|
||||||
|
|
||||||
|
for ext_id in &group_data.MemberExternalIds {
|
||||||
|
if let Some(user) = User::find_by_external_id(ext_id, &mut conn).await {
|
||||||
|
if let Some(user_org) = UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &mut conn).await
|
||||||
|
{
|
||||||
|
let mut group_user = GroupUser::new(group_uuid.clone(), user_org.uuid.clone());
|
||||||
|
group_user.save(&mut conn).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("Group support is disabled, groups will not be imported!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true)
|
||||||
|
if data.OverwriteExisting {
|
||||||
|
// Generate a HashSet to quickly verify if a member is listed or not.
|
||||||
|
let sync_members: HashSet<String> = data.Members.into_iter().map(|m| m.ExternalId).collect();
|
||||||
|
for user_org in UserOrganization::find_by_org(&org_id, &mut conn).await {
|
||||||
|
if let Some(user_external_id) =
|
||||||
|
User::find_by_uuid(&user_org.user_uuid, &mut conn).await.map(|u| u.external_id)
|
||||||
|
{
|
||||||
|
if user_external_id.is_some() && !sync_members.contains(&user_external_id.unwrap()) {
|
||||||
|
if user_org.atype == UserOrgType::Owner && user_org.status == UserOrgStatus::Confirmed as i32 {
|
||||||
|
// Removing owner, check that there is at least one other confirmed owner
|
||||||
|
if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn)
|
||||||
|
.await
|
||||||
|
<= 1
|
||||||
|
{
|
||||||
|
warn!("Can't delete the last owner");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user_org.delete(&mut conn).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PublicToken(String);
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for PublicToken {
|
||||||
|
type Error = &'static str;
|
||||||
|
|
||||||
|
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||||
|
let headers = request.headers();
|
||||||
|
// Get access_token
|
||||||
|
let access_token: &str = match headers.get_one("Authorization") {
|
||||||
|
Some(a) => match a.rsplit("Bearer ").next() {
|
||||||
|
Some(split) => split,
|
||||||
|
None => err_handler!("No access token provided"),
|
||||||
|
},
|
||||||
|
None => err_handler!("No access token provided"),
|
||||||
|
};
|
||||||
|
// Check JWT token is valid and get device and user from it
|
||||||
|
let claims = match auth::decode_api_org(access_token) {
|
||||||
|
Ok(claims) => claims,
|
||||||
|
Err(_) => err_handler!("Invalid claim"),
|
||||||
|
};
|
||||||
|
// Check if time is between claims.nbf and claims.exp
|
||||||
|
let time_now = Utc::now().naive_utc().timestamp();
|
||||||
|
if time_now < claims.nbf {
|
||||||
|
err_handler!("Token issued in the future");
|
||||||
|
}
|
||||||
|
if time_now > claims.exp {
|
||||||
|
err_handler!("Token expired");
|
||||||
|
}
|
||||||
|
// Check if claims.iss is host|claims.scope[0]
|
||||||
|
let host = match auth::Host::from_request(request).await {
|
||||||
|
Outcome::Success(host) => host,
|
||||||
|
_ => err_handler!("Error getting Host"),
|
||||||
|
};
|
||||||
|
let complete_host = format!("{}|{}", host.host, claims.scope[0]);
|
||||||
|
if complete_host != claims.iss {
|
||||||
|
err_handler!("Token not issued by this server");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if claims.sub is org_api_key.uuid
|
||||||
|
// Check if claims.client_sub is org_api_key.org_uuid
|
||||||
|
let conn = match DbConn::from_request(request).await {
|
||||||
|
Outcome::Success(conn) => conn,
|
||||||
|
_ => err_handler!("Error getting DB"),
|
||||||
|
};
|
||||||
|
let org_uuid = match claims.client_id.strip_prefix("organization.") {
|
||||||
|
Some(uuid) => uuid,
|
||||||
|
None => err_handler!("Malformed client_id"),
|
||||||
|
};
|
||||||
|
let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_uuid, &conn).await {
|
||||||
|
Some(org_api_key) => org_api_key,
|
||||||
|
None => err_handler!("Invalid client_id"),
|
||||||
|
};
|
||||||
|
if org_api_key.org_uuid != claims.client_sub {
|
||||||
|
err_handler!("Token not issued for this org");
|
||||||
|
}
|
||||||
|
if org_api_key.uuid != claims.sub {
|
||||||
|
err_handler!("Token not issued for this client");
|
||||||
|
}
|
||||||
|
|
||||||
|
Outcome::Success(PublicToken(claims.client_sub))
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ use crate::{
|
||||||
core::two_factor::{duo, email, email::EmailTokenData, yubikey},
|
core::two_factor::{duo, email, email::EmailTokenData, yubikey},
|
||||||
ApiResult, EmptyResult, JsonResult, JsonUpcase,
|
ApiResult, EmptyResult, JsonResult, JsonUpcase,
|
||||||
},
|
},
|
||||||
auth::{ClientHeaders, ClientIp},
|
auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp},
|
||||||
db::{models::*, DbConn},
|
db::{models::*, DbConn},
|
||||||
error::MapResult,
|
error::MapResult,
|
||||||
mail, util, CONFIG,
|
mail, util, CONFIG,
|
||||||
|
@ -276,16 +276,23 @@ async fn _api_key_login(
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
// Validate scope
|
|
||||||
let scope = data.scope.as_ref().unwrap();
|
|
||||||
if scope != "api" {
|
|
||||||
err!("Scope not supported")
|
|
||||||
}
|
|
||||||
let scope_vec = vec!["api".into()];
|
|
||||||
|
|
||||||
// Ratelimit the login
|
// Ratelimit the login
|
||||||
crate::ratelimit::check_limit_login(&ip.ip)?;
|
crate::ratelimit::check_limit_login(&ip.ip)?;
|
||||||
|
|
||||||
|
// Validate scope
|
||||||
|
match data.scope.as_ref().unwrap().as_ref() {
|
||||||
|
"api" => _user_api_key_login(data, user_uuid, conn, ip).await,
|
||||||
|
"api.organization" => _organization_api_key_login(data, conn, ip).await,
|
||||||
|
_ => err!("Scope not supported"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn _user_api_key_login(
|
||||||
|
data: ConnectData,
|
||||||
|
user_uuid: &mut Option<String>,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
ip: &ClientIp,
|
||||||
|
) -> JsonResult {
|
||||||
// Get the user via the client_id
|
// Get the user via the client_id
|
||||||
let client_id = data.client_id.as_ref().unwrap();
|
let client_id = data.client_id.as_ref().unwrap();
|
||||||
let client_user_uuid = match client_id.strip_prefix("user.") {
|
let client_user_uuid = match client_id.strip_prefix("user.") {
|
||||||
|
@ -342,6 +349,7 @@ async fn _api_key_login(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common
|
// Common
|
||||||
|
let scope_vec = vec!["api".into()];
|
||||||
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
||||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
|
let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
|
||||||
device.save(conn).await?;
|
device.save(conn).await?;
|
||||||
|
@ -362,13 +370,43 @@ async fn _api_key_login(
|
||||||
"KdfMemory": user.client_kdf_memory,
|
"KdfMemory": user.client_kdf_memory,
|
||||||
"KdfParallelism": user.client_kdf_parallelism,
|
"KdfParallelism": user.client_kdf_parallelism,
|
||||||
"ResetMasterPassword": false, // TODO: Same as above
|
"ResetMasterPassword": false, // TODO: Same as above
|
||||||
"scope": scope,
|
"scope": "api",
|
||||||
"unofficialServer": true,
|
"unofficialServer": true,
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn _organization_api_key_login(data: ConnectData, conn: &mut DbConn, ip: &ClientIp) -> JsonResult {
|
||||||
|
// Get the org via the client_id
|
||||||
|
let client_id = data.client_id.as_ref().unwrap();
|
||||||
|
let org_uuid = match client_id.strip_prefix("organization.") {
|
||||||
|
Some(uuid) => uuid,
|
||||||
|
None => err!("Malformed client_id", format!("IP: {}.", ip.ip)),
|
||||||
|
};
|
||||||
|
let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_uuid, conn).await {
|
||||||
|
Some(org_api_key) => org_api_key,
|
||||||
|
None => err!("Invalid client_id", format!("IP: {}.", ip.ip)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check API key.
|
||||||
|
let client_secret = data.client_secret.as_ref().unwrap();
|
||||||
|
if !org_api_key.check_valid_api_key(client_secret) {
|
||||||
|
err!("Incorrect client_secret", format!("IP: {}. Organization: {}.", ip.ip, org_api_key.org_uuid))
|
||||||
|
}
|
||||||
|
|
||||||
|
let claim = generate_organization_api_key_login_claims(org_api_key.uuid, org_api_key.org_uuid);
|
||||||
|
let access_token = crate::auth::encode_jwt(&claim);
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"access_token": access_token,
|
||||||
|
"expires_in": 3600,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"scope": "api.organization",
|
||||||
|
"unofficialServer": true,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieves an existing device or creates a new device from ConnectData and the User
|
/// Retrieves an existing device or creates a new device from ConnectData and the User
|
||||||
async fn get_device(data: &ConnectData, conn: &mut DbConn, user: &User) -> (Device, bool) {
|
async fn get_device(data: &ConnectData, conn: &mut DbConn, user: &User) -> (Device, bool) {
|
||||||
// On iOS, device_type sends "iOS", on others it sends a number
|
// On iOS, device_type sends "iOS", on others it sends a number
|
||||||
|
|
34
src/auth.rs
34
src/auth.rs
|
@ -23,6 +23,7 @@ static JWT_DELETE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|delete", CONFI
|
||||||
static JWT_VERIFYEMAIL_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|verifyemail", CONFIG.domain_origin()));
|
static JWT_VERIFYEMAIL_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|verifyemail", CONFIG.domain_origin()));
|
||||||
static JWT_ADMIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|admin", CONFIG.domain_origin()));
|
static JWT_ADMIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|admin", CONFIG.domain_origin()));
|
||||||
static JWT_SEND_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|send", CONFIG.domain_origin()));
|
static JWT_SEND_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|send", CONFIG.domain_origin()));
|
||||||
|
static JWT_ORG_API_KEY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|api.organization", CONFIG.domain_origin()));
|
||||||
|
|
||||||
static PRIVATE_RSA_KEY: Lazy<EncodingKey> = Lazy::new(|| {
|
static PRIVATE_RSA_KEY: Lazy<EncodingKey> = Lazy::new(|| {
|
||||||
let key =
|
let key =
|
||||||
|
@ -93,6 +94,10 @@ pub fn decode_send(token: &str) -> Result<BasicJwtClaims, Error> {
|
||||||
decode_jwt(token, JWT_SEND_ISSUER.to_string())
|
decode_jwt(token, JWT_SEND_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn decode_api_org(token: &str) -> Result<OrgApiKeyLoginJwtClaims, Error> {
|
||||||
|
decode_jwt(token, JWT_ORG_API_KEY_ISSUER.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct LoginJwtClaims {
|
pub struct LoginJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
|
@ -200,6 +205,35 @@ pub fn generate_emergency_access_invite_claims(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct OrgApiKeyLoginJwtClaims {
|
||||||
|
// Not before
|
||||||
|
pub nbf: i64,
|
||||||
|
// Expiration time
|
||||||
|
pub exp: i64,
|
||||||
|
// Issuer
|
||||||
|
pub iss: String,
|
||||||
|
// Subject
|
||||||
|
pub sub: String,
|
||||||
|
|
||||||
|
pub client_id: String,
|
||||||
|
pub client_sub: String,
|
||||||
|
pub scope: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_organization_api_key_login_claims(uuid: String, org_id: String) -> OrgApiKeyLoginJwtClaims {
|
||||||
|
let time_now = Utc::now().naive_utc();
|
||||||
|
OrgApiKeyLoginJwtClaims {
|
||||||
|
nbf: time_now.timestamp(),
|
||||||
|
exp: (time_now + Duration::hours(1)).timestamp(),
|
||||||
|
iss: JWT_ORG_API_KEY_ISSUER.to_string(),
|
||||||
|
sub: uuid,
|
||||||
|
client_id: format!("organization.{org_id}"),
|
||||||
|
client_sub: org_id,
|
||||||
|
scope: vec!["api.organization".into()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct BasicJwtClaims {
|
pub struct BasicJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
|
||||||
use crate::CONFIG;
|
use crate::{crypto, CONFIG};
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
|
@ -47,9 +47,7 @@ impl Device {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_twofactor_remember(&mut self) -> String {
|
pub fn refresh_twofactor_remember(&mut self) -> String {
|
||||||
use crate::crypto;
|
|
||||||
use data_encoding::BASE64;
|
use data_encoding::BASE64;
|
||||||
|
|
||||||
let twofactor_remember = crypto::encode_random_bytes::<180>(BASE64);
|
let twofactor_remember = crypto::encode_random_bytes::<180>(BASE64);
|
||||||
self.twofactor_remember = Some(twofactor_remember.clone());
|
self.twofactor_remember = Some(twofactor_remember.clone());
|
||||||
|
|
||||||
|
@ -68,9 +66,7 @@ impl Device {
|
||||||
) -> (String, i64) {
|
) -> (String, i64) {
|
||||||
// If there is no refresh token, we create one
|
// If there is no refresh token, we create one
|
||||||
if self.refresh_token.is_empty() {
|
if self.refresh_token.is_empty() {
|
||||||
use crate::crypto;
|
|
||||||
use data_encoding::BASE64URL;
|
use data_encoding::BASE64URL;
|
||||||
|
|
||||||
self.refresh_token = crypto::encode_random_bytes::<64>(BASE64URL);
|
self.refresh_token = crypto::encode_random_bytes::<64>(BASE64URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ db_object! {
|
||||||
pub organizations_uuid: String,
|
pub organizations_uuid: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub access_all: bool,
|
pub access_all: bool,
|
||||||
external_id: Option<String>,
|
pub external_id: Option<String>,
|
||||||
pub creation_date: NaiveDateTime,
|
pub creation_date: NaiveDateTime,
|
||||||
pub revision_date: NaiveDateTime,
|
pub revision_date: NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
@ -107,10 +107,6 @@ impl Group {
|
||||||
None => self.external_id = None,
|
None => self.external_id = None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_external_id(&self) -> Option<String> {
|
|
||||||
self.external_id.clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CollectionGroup {
|
impl CollectionGroup {
|
||||||
|
@ -214,6 +210,15 @@ impl Group {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_external_id(id: &str, conn: &mut DbConn) -> Option<Self> {
|
||||||
|
db_run! { conn: {
|
||||||
|
groups::table
|
||||||
|
.filter(groups::external_id.eq(id))
|
||||||
|
.first::<GroupDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
//Returns all organizations the user has full access to
|
//Returns all organizations the user has full access to
|
||||||
pub async fn gather_user_organizations_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec<String> {
|
pub async fn gather_user_organizations_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec<String> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
|
|
|
@ -24,7 +24,7 @@ pub use self::favorite::Favorite;
|
||||||
pub use self::folder::{Folder, FolderCipher};
|
pub use self::folder::{Folder, FolderCipher};
|
||||||
pub use self::group::{CollectionGroup, Group, GroupUser};
|
pub use self::group::{CollectionGroup, Group, GroupUser};
|
||||||
pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType};
|
pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType};
|
||||||
pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
|
pub use self::organization::{Organization, OrganizationApiKey, UserOrgStatus, UserOrgType, UserOrganization};
|
||||||
pub use self::send::{Send, SendType};
|
pub use self::send::{Send, SendType};
|
||||||
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
||||||
pub use self::two_factor_incomplete::TwoFactorIncomplete;
|
pub use self::two_factor_incomplete::TwoFactorIncomplete;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
@ -31,6 +32,17 @@ db_object! {
|
||||||
pub atype: i32,
|
pub atype: i32,
|
||||||
pub reset_password_key: Option<String>,
|
pub reset_password_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
|
#[diesel(table_name = organization_api_key)]
|
||||||
|
#[diesel(primary_key(uuid, org_uuid))]
|
||||||
|
pub struct OrganizationApiKey {
|
||||||
|
pub uuid: String,
|
||||||
|
pub org_uuid: String,
|
||||||
|
pub atype: i32,
|
||||||
|
pub api_key: String,
|
||||||
|
pub revision_date: NaiveDateTime,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/OrganizationUserStatusType.cs
|
// https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/OrganizationUserStatusType.cs
|
||||||
|
@ -157,7 +169,7 @@ impl Organization {
|
||||||
"UseSso": false, // Not supported
|
"UseSso": false, // Not supported
|
||||||
// "UseKeyConnector": false, // Not supported
|
// "UseKeyConnector": false, // Not supported
|
||||||
"SelfHost": true,
|
"SelfHost": true,
|
||||||
"UseApi": false, // Not supported
|
"UseApi": true,
|
||||||
"HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(),
|
"HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(),
|
||||||
"UseResetPassword": CONFIG.mail_enabled(),
|
"UseResetPassword": CONFIG.mail_enabled(),
|
||||||
|
|
||||||
|
@ -212,6 +224,23 @@ impl UserOrganization {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OrganizationApiKey {
|
||||||
|
pub fn new(org_uuid: String, api_key: String) -> Self {
|
||||||
|
Self {
|
||||||
|
uuid: crate::util::get_uuid(),
|
||||||
|
|
||||||
|
org_uuid,
|
||||||
|
atype: 0, // Type 0 is the default and only type we support currently
|
||||||
|
api_key,
|
||||||
|
revision_date: Utc::now().naive_utc(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_valid_api_key(&self, api_key: &str) -> bool {
|
||||||
|
crate::crypto::ct_eq(&self.api_key, api_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
|
@ -311,7 +340,7 @@ impl UserOrganization {
|
||||||
"UseTotp": true,
|
"UseTotp": true,
|
||||||
// "UseScim": false, // Not supported (Not AGPLv3 Licensed)
|
// "UseScim": false, // Not supported (Not AGPLv3 Licensed)
|
||||||
"UsePolicies": true,
|
"UsePolicies": true,
|
||||||
"UseApi": false, // Not supported
|
"UseApi": true,
|
||||||
"SelfHost": true,
|
"SelfHost": true,
|
||||||
"HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(),
|
"HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(),
|
||||||
"ResetPasswordEnrolled": self.reset_password_key.is_some(),
|
"ResetPasswordEnrolled": self.reset_password_key.is_some(),
|
||||||
|
@ -481,7 +510,7 @@ impl UserOrganization {
|
||||||
.set(UserOrganizationDb::to_db(self))
|
.set(UserOrganizationDb::to_db(self))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map_res("Error adding user to organization")
|
.map_res("Error adding user to organization")
|
||||||
}
|
},
|
||||||
Err(e) => Err(e.into()),
|
Err(e) => Err(e.into()),
|
||||||
}.map_res("Error adding user to organization")
|
}.map_res("Error adding user to organization")
|
||||||
}
|
}
|
||||||
|
@ -750,6 +779,50 @@ impl UserOrganization {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OrganizationApiKey {
|
||||||
|
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||||
|
db_run! { conn:
|
||||||
|
sqlite, mysql {
|
||||||
|
match diesel::replace_into(organization_api_key::table)
|
||||||
|
.values(OrganizationApiKeyDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
// Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
|
||||||
|
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
|
||||||
|
diesel::update(organization_api_key::table)
|
||||||
|
.filter(organization_api_key::uuid.eq(&self.uuid))
|
||||||
|
.set(OrganizationApiKeyDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving organization")
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}.map_res("Error saving organization")
|
||||||
|
|
||||||
|
}
|
||||||
|
postgresql {
|
||||||
|
let value = OrganizationApiKeyDb::to_db(self);
|
||||||
|
diesel::insert_into(organization_api_key::table)
|
||||||
|
.values(&value)
|
||||||
|
.on_conflict(organization_api_key::uuid)
|
||||||
|
.do_update()
|
||||||
|
.set(&value)
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving organization")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_org_uuid(org_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
|
db_run! { conn: {
|
||||||
|
organization_api_key::table
|
||||||
|
.filter(organization_api_key::org_uuid.eq(org_uuid))
|
||||||
|
.first::<OrganizationApiKeyDb>(conn)
|
||||||
|
.ok().from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -50,6 +50,8 @@ db_object! {
|
||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
|
|
||||||
pub avatar_color: Option<String>,
|
pub avatar_color: Option<String>,
|
||||||
|
|
||||||
|
pub external_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Insertable)]
|
#[derive(Identifiable, Queryable, Insertable)]
|
||||||
|
@ -126,6 +128,8 @@ impl User {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
|
|
||||||
avatar_color: None,
|
avatar_color: None,
|
||||||
|
|
||||||
|
external_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,6 +154,18 @@ impl User {
|
||||||
matches!(self.api_key, Some(ref api_key) if crate::crypto::ct_eq(api_key, key))
|
matches!(self.api_key, Some(ref api_key) if crate::crypto::ct_eq(api_key, key))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_external_id(&mut self, external_id: Option<String>) {
|
||||||
|
//Check if external id is empty. We don't want to have
|
||||||
|
//empty strings in the database
|
||||||
|
let mut ext_id: Option<String> = None;
|
||||||
|
if let Some(external_id) = external_id {
|
||||||
|
if !external_id.is_empty() {
|
||||||
|
ext_id = Some(external_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.external_id = ext_id;
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the password hash generated
|
/// Set the password hash generated
|
||||||
/// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different.
|
/// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different.
|
||||||
///
|
///
|
||||||
|
@ -376,6 +392,12 @@ impl User {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_external_id(id: &str, conn: &mut DbConn) -> Option<Self> {
|
||||||
|
db_run! {conn: {
|
||||||
|
users::table.filter(users::external_id.eq(id)).first::<UserDb>(conn).ok().from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_all(conn: &mut DbConn) -> Vec<Self> {
|
pub async fn get_all(conn: &mut DbConn) -> Vec<Self> {
|
||||||
db_run! {conn: {
|
db_run! {conn: {
|
||||||
users::table.load::<UserDb>(conn).expect("Error loading users").from_db()
|
users::table.load::<UserDb>(conn).expect("Error loading users").from_db()
|
||||||
|
|
|
@ -204,6 +204,7 @@ table! {
|
||||||
client_kdf_parallelism -> Nullable<Integer>,
|
client_kdf_parallelism -> Nullable<Integer>,
|
||||||
api_key -> Nullable<Text>,
|
api_key -> Nullable<Text>,
|
||||||
avatar_color -> Nullable<Text>,
|
avatar_color -> Nullable<Text>,
|
||||||
|
external_id -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,6 +230,16 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
organization_api_key (uuid, org_uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
org_uuid -> Text,
|
||||||
|
atype -> Integer,
|
||||||
|
api_key -> Text,
|
||||||
|
revision_date -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
emergency_access (uuid) {
|
emergency_access (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
@ -292,6 +303,7 @@ joinable!(users_collections -> collections (collection_uuid));
|
||||||
joinable!(users_collections -> users (user_uuid));
|
joinable!(users_collections -> users (user_uuid));
|
||||||
joinable!(users_organizations -> organizations (org_uuid));
|
joinable!(users_organizations -> organizations (org_uuid));
|
||||||
joinable!(users_organizations -> users (user_uuid));
|
joinable!(users_organizations -> users (user_uuid));
|
||||||
|
joinable!(organization_api_key -> organizations (org_uuid));
|
||||||
joinable!(emergency_access -> users (grantor_uuid));
|
joinable!(emergency_access -> users (grantor_uuid));
|
||||||
joinable!(groups -> organizations (organizations_uuid));
|
joinable!(groups -> organizations (organizations_uuid));
|
||||||
joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
||||||
|
@ -316,6 +328,7 @@ allow_tables_to_appear_in_same_query!(
|
||||||
users,
|
users,
|
||||||
users_collections,
|
users_collections,
|
||||||
users_organizations,
|
users_organizations,
|
||||||
|
organization_api_key,
|
||||||
emergency_access,
|
emergency_access,
|
||||||
groups,
|
groups,
|
||||||
groups_users,
|
groups_users,
|
||||||
|
|
|
@ -204,6 +204,7 @@ table! {
|
||||||
client_kdf_parallelism -> Nullable<Integer>,
|
client_kdf_parallelism -> Nullable<Integer>,
|
||||||
api_key -> Nullable<Text>,
|
api_key -> Nullable<Text>,
|
||||||
avatar_color -> Nullable<Text>,
|
avatar_color -> Nullable<Text>,
|
||||||
|
external_id -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,6 +230,16 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
organization_api_key (uuid, org_uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
org_uuid -> Text,
|
||||||
|
atype -> Integer,
|
||||||
|
api_key -> Text,
|
||||||
|
revision_date -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
emergency_access (uuid) {
|
emergency_access (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
@ -292,6 +303,7 @@ joinable!(users_collections -> collections (collection_uuid));
|
||||||
joinable!(users_collections -> users (user_uuid));
|
joinable!(users_collections -> users (user_uuid));
|
||||||
joinable!(users_organizations -> organizations (org_uuid));
|
joinable!(users_organizations -> organizations (org_uuid));
|
||||||
joinable!(users_organizations -> users (user_uuid));
|
joinable!(users_organizations -> users (user_uuid));
|
||||||
|
joinable!(organization_api_key -> organizations (org_uuid));
|
||||||
joinable!(emergency_access -> users (grantor_uuid));
|
joinable!(emergency_access -> users (grantor_uuid));
|
||||||
joinable!(groups -> organizations (organizations_uuid));
|
joinable!(groups -> organizations (organizations_uuid));
|
||||||
joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
||||||
|
@ -316,6 +328,7 @@ allow_tables_to_appear_in_same_query!(
|
||||||
users,
|
users,
|
||||||
users_collections,
|
users_collections,
|
||||||
users_organizations,
|
users_organizations,
|
||||||
|
organization_api_key,
|
||||||
emergency_access,
|
emergency_access,
|
||||||
groups,
|
groups,
|
||||||
groups_users,
|
groups_users,
|
||||||
|
|
|
@ -204,6 +204,7 @@ table! {
|
||||||
client_kdf_parallelism -> Nullable<Integer>,
|
client_kdf_parallelism -> Nullable<Integer>,
|
||||||
api_key -> Nullable<Text>,
|
api_key -> Nullable<Text>,
|
||||||
avatar_color -> Nullable<Text>,
|
avatar_color -> Nullable<Text>,
|
||||||
|
external_id -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,6 +230,16 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
organization_api_key (uuid, org_uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
org_uuid -> Text,
|
||||||
|
atype -> Integer,
|
||||||
|
api_key -> Text,
|
||||||
|
revision_date -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
emergency_access (uuid) {
|
emergency_access (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
@ -293,6 +304,7 @@ joinable!(users_collections -> users (user_uuid));
|
||||||
joinable!(users_organizations -> organizations (org_uuid));
|
joinable!(users_organizations -> organizations (org_uuid));
|
||||||
joinable!(users_organizations -> users (user_uuid));
|
joinable!(users_organizations -> users (user_uuid));
|
||||||
joinable!(users_organizations -> ciphers (org_uuid));
|
joinable!(users_organizations -> ciphers (org_uuid));
|
||||||
|
joinable!(organization_api_key -> organizations (org_uuid));
|
||||||
joinable!(emergency_access -> users (grantor_uuid));
|
joinable!(emergency_access -> users (grantor_uuid));
|
||||||
joinable!(groups -> organizations (organizations_uuid));
|
joinable!(groups -> organizations (organizations_uuid));
|
||||||
joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
||||||
|
@ -317,6 +329,7 @@ allow_tables_to_appear_in_same_query!(
|
||||||
users,
|
users,
|
||||||
users_collections,
|
users_collections,
|
||||||
users_organizations,
|
users_organizations,
|
||||||
|
organization_api_key,
|
||||||
emergency_access,
|
emergency_access,
|
||||||
groups,
|
groups,
|
||||||
groups_users,
|
groups_users,
|
||||||
|
|
Laden …
In neuem Issue referenzieren