Compare commits

...

7 Commits

Author SHA1 Message Date
Daniel García
a7a479623c Merge pull request #1087 from jjlin/org-creation-users
Add support for restricting org creation to certain users
2020-08-08 16:20:15 +02:00
Daniel García
83dff9ae6e Merge pull request #1083 from jjlin/global-domains
Add a script to auto-generate the global equivalent domains JSON file
2020-08-08 16:19:30 +02:00
Daniel García
6b2cc5a3ee Merge pull request #1089 from jjlin/master
Don't push `latest-arm32v6` tag for MySQL and PostgreSQL images
2020-08-07 20:39:17 +02:00
Jeremy Lin
5247e0d773 Don't push latest-arm32v6 tag for MySQL and PostgreSQL images 2020-08-07 10:15:15 -07:00
Jeremy Lin
05b308b8b4 Sync global_domains.json with upstream 2020-08-06 12:13:40 -07:00
Jeremy Lin
9621278fca Add a script to auto-generate the global equivalent domains JSON file
The script works by reading the relevant files from the upstream Bitwarden
source repo and generating a matching JSON file. It could potentially be
integrated into the build/release process, but for now it can be run manually
as needed.
2020-08-06 12:12:32 -07:00
Jeremy Lin
570d6c8bf9 Add support for restricting org creation to certain users 2020-08-05 22:35:29 -07:00
6 changed files with 153 additions and 43 deletions

View File

@@ -118,6 +118,14 @@
## even if SIGNUPS_ALLOWED is set to false ## even if SIGNUPS_ALLOWED is set to false
# SIGNUPS_DOMAINS_WHITELIST=example.com,example.net,example.org # SIGNUPS_DOMAINS_WHITELIST=example.com,example.net,example.org
## Controls which users can create new orgs.
## Blank or 'all' means all users can create orgs (this is the default):
# ORG_CREATION_USERS=
## 'none' means no users can create orgs:
# ORG_CREATION_USERS=none
## A comma-separated list means only those users can create orgs:
# ORG_CREATION_USERS=admin1@example.com,admin2@example.com
## Token for the admin interface, preferably use a long random string ## Token for the admin interface, preferably use a long random string
## One option is to use 'openssl rand -base64 48' ## One option is to use 'openssl rand -base64 48'
## If not set, the admin panel is disabled ## If not set, the admin panel is disabled

View File

@@ -41,13 +41,18 @@ if [[ "${DOCKER_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
# auto-select that image on Armv6 platforms like Raspberry Pi 1 and Zero # auto-select that image on Armv6 platforms like Raspberry Pi 1 and Zero
# (https://github.com/moby/moby/issues/41017). # (https://github.com/moby/moby/issues/41017).
# #
# Add this tag only for the SQLite image, as the MySQL and PostgreSQL
# builds don't currently work on non-amd64 arches.
#
# TODO: Also add an `alpine-arm32v6` tag if multi-arch support for # TODO: Also add an `alpine-arm32v6` tag if multi-arch support for
# Alpine-based bitwarden_rs images is implemented before this Docker # Alpine-based bitwarden_rs images is implemented before this Docker
# issue is fixed. # issue is fixed.
if [[ ${DOCKER_REPO} == *server ]]; then
docker tag "${DOCKER_REPO}:${DOCKER_TAG}-arm32v6" "${DOCKER_REPO}:latest-arm32v6" docker tag "${DOCKER_REPO}:${DOCKER_TAG}-arm32v6" "${DOCKER_REPO}:latest-arm32v6"
docker push "${DOCKER_REPO}:latest-arm32v6" docker push "${DOCKER_REPO}:latest-arm32v6"
fi fi
fi fi
fi
for manifest_list in "${manifest_lists[@]}"; do for manifest_list in "${manifest_lists[@]}"; do
# Create the (multi-arch) manifest list of arch-specific images. # Create the (multi-arch) manifest list of arch-specific images.

View File

@@ -76,6 +76,10 @@ struct NewCollectionData {
#[post("/organizations", data = "<data>")] #[post("/organizations", data = "<data>")]
fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn) -> JsonResult { fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn) -> JsonResult {
if !CONFIG.is_org_creation_allowed(&headers.user.email) {
err!("User not allowed to create organizations")
}
let data: OrgData = data.into_inner().data; let data: OrgData = data.into_inner().data;
let org = Organization::new(data.Name, data.BillingEmail); let org = Organization::new(data.Name, data.BillingEmail);

View File

@@ -115,6 +115,7 @@ macro_rules! make_config {
config.domain_set = _domain_set; config.domain_set = _domain_set;
config.signups_domains_whitelist = config.signups_domains_whitelist.trim().to_lowercase(); config.signups_domains_whitelist = config.signups_domains_whitelist.trim().to_lowercase();
config.org_creation_users = config.org_creation_users.trim().to_lowercase();
config config
} }
@@ -276,6 +277,9 @@ make_config! {
signups_verify_resend_limit: u32, true, def, 6; signups_verify_resend_limit: u32, true, def, 6;
/// Email domain whitelist |> Allow signups only from this list of comma-separated domains, even when signups are otherwise disabled /// Email domain whitelist |> Allow signups only from this list of comma-separated domains, even when signups are otherwise disabled
signups_domains_whitelist: String, true, def, "".to_string(); signups_domains_whitelist: String, true, def, "".to_string();
/// Org creation users |> Allow org creation only by this list of comma-separated user emails.
/// Blank or 'all' means all users can create orgs; 'none' means no users can create orgs.
org_creation_users: String, true, def, "".to_string();
/// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are otherwise disabled /// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are otherwise disabled
invitations_allowed: bool, true, def, true; invitations_allowed: bool, true, def, true;
/// Password iterations |> Number of server-side passwords hashing iterations. /// Password iterations |> Number of server-side passwords hashing iterations.
@@ -442,6 +446,13 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
err!("`SIGNUPS_DOMAINS_WHITELIST` contains empty tokens"); err!("`SIGNUPS_DOMAINS_WHITELIST` contains empty tokens");
} }
let org_creation_users = cfg.org_creation_users.trim().to_lowercase();
if !(org_creation_users.is_empty() || org_creation_users == "all" || org_creation_users == "none") {
if org_creation_users.split(',').any(|u| !u.contains('@')) {
err!("`ORG_CREATION_USERS` contains invalid email addresses");
}
}
if let Some(ref token) = cfg.admin_token { if let Some(ref token) = cfg.admin_token {
if token.trim().is_empty() && !cfg.disable_admin_token { if token.trim().is_empty() && !cfg.disable_admin_token {
println!("[WARNING] `ADMIN_TOKEN` is enabled but has an empty value, so the admin page will be disabled."); println!("[WARNING] `ADMIN_TOKEN` is enabled but has an empty value, so the admin page will be disabled.");
@@ -592,6 +603,19 @@ impl Config {
} }
} }
/// Tests whether the specified user is allowed to create an organization.
pub fn is_org_creation_allowed(&self, email: &str) -> bool {
let users = self.org_creation_users();
if users == "" || users == "all" {
true
} else if users == "none" {
false
} else {
let email = email.to_lowercase();
users.split(',').any(|u| u.trim() == email)
}
}
pub fn delete_user_config(&self) -> Result<(), Error> { pub fn delete_user_config(&self) -> Result<(), Error> {
crate::util::delete_file(&CONFIG_FILE)?; crate::util::delete_file(&CONFIG_FILE)?;

View File

@@ -39,8 +39,7 @@
"Type": 1, "Type": 1,
"Domains": [ "Domains": [
"apple.com", "apple.com",
"icloud.com", "icloud.com"
"tv.apple.com"
], ],
"Excluded": false "Excluded": false
}, },
@@ -106,6 +105,7 @@
"passport.net", "passport.net",
"windows.com", "windows.com",
"microsoftonline.com", "microsoftonline.com",
"office.com",
"office365.com", "office365.com",
"microsoftstore.com", "microsoftstore.com",
"xbox.com", "xbox.com",
@@ -193,7 +193,12 @@
"amazon.it", "amazon.it",
"amazon.com.au", "amazon.com.au",
"amazon.co.nz", "amazon.co.nz",
"amazon.in" "amazon.in",
"amazon.com.mx",
"amazon.nl",
"amazon.sg",
"amazon.com.tr",
"amazon.ae"
], ],
"Excluded": false "Excluded": false
}, },
@@ -386,8 +391,7 @@
"alibaba.com", "alibaba.com",
"aliexpress.com", "aliexpress.com",
"aliyun.com", "aliyun.com",
"net.cn", "net.cn"
"www.net.cn"
], ],
"Excluded": false "Excluded": false
}, },
@@ -717,41 +721,27 @@
"eventbrite.ca", "eventbrite.ca",
"eventbrite.ch", "eventbrite.ch",
"eventbrite.cl", "eventbrite.cl",
"eventbrite.co.id", "eventbrite.co",
"eventbrite.co.in",
"eventbrite.co.kr",
"eventbrite.co.nz", "eventbrite.co.nz",
"eventbrite.co.uk", "eventbrite.co.uk",
"eventbrite.co.ve",
"eventbrite.com", "eventbrite.com",
"eventbrite.com.ar",
"eventbrite.com.au", "eventbrite.com.au",
"eventbrite.com.bo",
"eventbrite.com.br", "eventbrite.com.br",
"eventbrite.com.co", "eventbrite.com.mx",
"eventbrite.com.hk",
"eventbrite.com.hn",
"eventbrite.com.pe", "eventbrite.com.pe",
"eventbrite.com.sg",
"eventbrite.com.tr",
"eventbrite.com.tw",
"eventbrite.cz",
"eventbrite.de", "eventbrite.de",
"eventbrite.dk", "eventbrite.dk",
"eventbrite.es",
"eventbrite.fi", "eventbrite.fi",
"eventbrite.fr", "eventbrite.fr",
"eventbrite.gy", "eventbrite.hk",
"eventbrite.hu",
"eventbrite.ie", "eventbrite.ie",
"eventbrite.is",
"eventbrite.it", "eventbrite.it",
"eventbrite.jp",
"eventbrite.mx",
"eventbrite.nl", "eventbrite.nl",
"eventbrite.no",
"eventbrite.pl",
"eventbrite.pt", "eventbrite.pt",
"eventbrite.ru", "eventbrite.se",
"eventbrite.se" "eventbrite.sg"
], ],
"Excluded": false "Excluded": false
}, },
@@ -769,15 +759,6 @@
}, },
{ {
"Type": 75, "Type": 75,
"Domains": [
"netcup.de",
"netcup.eu",
"customercontrolpanel.de"
],
"Excluded": false
},
{
"Type": 76,
"Domains": [ "Domains": [
"docusign.com", "docusign.com",
"docusign.net" "docusign.net"
@@ -785,7 +766,7 @@
"Excluded": false "Excluded": false
}, },
{ {
"Type": 77, "Type": 76,
"Domains": [ "Domains": [
"envato.com", "envato.com",
"themeforest.net", "themeforest.net",
@@ -799,7 +780,7 @@
"Excluded": false "Excluded": false
}, },
{ {
"Type": 78, "Type": 77,
"Domains": [ "Domains": [
"x10hosting.com", "x10hosting.com",
"x10premium.com" "x10premium.com"
@@ -807,7 +788,7 @@
"Excluded": false "Excluded": false
}, },
{ {
"Type": 79, "Type": 78,
"Domains": [ "Domains": [
"dnsomatic.com", "dnsomatic.com",
"opendns.com", "opendns.com",
@@ -816,7 +797,7 @@
"Excluded": false "Excluded": false
}, },
{ {
"Type": 80, "Type": 79,
"Domains": [ "Domains": [
"cagreatamerica.com", "cagreatamerica.com",
"canadaswonderland.com", "canadaswonderland.com",
@@ -835,11 +816,19 @@
"Excluded": false "Excluded": false
}, },
{ {
"Type": 81, "Type": 80,
"Domains": [ "Domains": [
"ubnt.com", "ubnt.com",
"ui.com" "ui.com"
], ],
"Excluded": false "Excluded": false
},
{
"Type": 81,
"Domains": [
"discordapp.com",
"discord.com"
],
"Excluded": false
} }
] ]

80
tools/global_domains.py Executable file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
#
# This script generates a global equivalent domains JSON file from
# the upstream Bitwarden source repo.
#
import json
import re
import sys
import urllib.request
from collections import OrderedDict
if len(sys.argv) != 2:
print("usage: %s <OUTPUT-FILE>" % sys.argv[0])
print()
print("This script generates a global equivalent domains JSON file from")
print("the upstream Bitwarden source repo.")
sys.exit(1)
OUTPUT_FILE = sys.argv[1]
BASE_URL = 'https://github.com/bitwarden/server/raw/master'
ENUMS_URL = '%s/src/Core/Enums/GlobalEquivalentDomainsType.cs' % BASE_URL
DOMAIN_LISTS_URL = '%s/src/Core/Utilities/StaticStore.cs' % BASE_URL
# Enum lines look like:
#
# EnumName0 = 0,
# EnumName1 = 1,
#
ENUM_RE = re.compile(
r'\s*' # Leading whitespace (optional).
r'([_0-9a-zA-Z]+)' # Enum name (capture group 1).
r'\s*=\s*' # '=' with optional surrounding whitespace.
r'([0-9]+)' # Enum value (capture group 2).
)
# Global domains lines look like:
#
# GlobalDomains.Add(GlobalEquivalentDomainsType.EnumName, new List<string> { "x.com", "y.com" });
#
DOMAIN_LIST_RE = re.compile(
r'\s*' # Leading whitespace (optional).
r'GlobalDomains\.Add\(GlobalEquivalentDomainsType\.'
r'([_0-9a-zA-Z]+)' # Enum name (capture group 1).
r'\s*,\s*new List<string>\s*{'
r'([^}]+)' # Domain list (capture group 2).
r'}\);'
)
enums = dict()
domain_lists = OrderedDict()
# Read in the enum names and values.
with urllib.request.urlopen(ENUMS_URL) as response:
for ln in response.read().decode('utf-8').split('\n'):
m = ENUM_RE.match(ln)
if m:
enums[m.group(1)] = int(m.group(2))
# Read in the domain lists.
with urllib.request.urlopen(DOMAIN_LISTS_URL) as response:
for ln in response.read().decode('utf-8').split('\n'):
m = DOMAIN_LIST_RE.match(ln)
if m:
# Strip double quotes and extraneous spaces in each domain.
domain_lists[m.group(1)] = [d.strip(' "') for d in m.group(2).split(",")]
# Build the global domains data structure.
global_domains = []
for name, domain_list in domain_lists.items():
entry = OrderedDict()
entry["Type"] = enums[name]
entry["Domains"] = domain_list
entry["Excluded"] = False
global_domains.append(entry)
# Write out the global domains JSON file.
with open(OUTPUT_FILE, 'w') as f:
json.dump(global_domains, f, indent=2)