diff --git a/Makefile b/Makefile index 362b8d105e1..02022680f4e 100644 --- a/Makefile +++ b/Makefile @@ -253,8 +253,8 @@ git-add-cassandra-schema: db-reset git-add-cassandra-schema-impl .PHONY: git-add-cassandra-schema-impl git-add-cassandra-schema-impl: $(eval CASSANDRA_CONTAINER := $(shell docker ps | grep '/cassandra:' | perl -ne '/^(\S+)\s/ && print $$1')) - ( echo '-- automatically generated with `make git-add-cassandra-schema`' ; docker exec -i $(CASSANDRA_CONTAINER) /usr/bin/cqlsh -e "DESCRIBE schema;" ) > ./docs/reference/cassandra-schema.cql - git add ./docs/reference/cassandra-schema.cql + ( echo '-- automatically generated with `make git-add-cassandra-schema`' ; docker exec -i $(CASSANDRA_CONTAINER) /usr/bin/cqlsh -e "DESCRIBE schema;" ) > ./cassandra-schema.cql + git add ./cassandra-schema.cql .PHONY: cqlsh cqlsh: diff --git a/cassandra-schema.cql b/cassandra-schema.cql new file mode 100644 index 00000000000..5afd359b05e --- /dev/null +++ b/cassandra-schema.cql @@ -0,0 +1,1898 @@ +-- automatically generated with `make git-add-cassandra-schema` + +CREATE KEYSPACE galley_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; + +CREATE TYPE galley_test.permissions ( + self bigint, + copy bigint +); + +CREATE TYPE galley_test.pubkey ( + typ int, + size int, + pem blob +); + +CREATE TABLE galley_test.meta ( + id int, + version int, + date timestamp, + descr text, + PRIMARY KEY (id, version) +) WITH CLUSTERING ORDER BY (version ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.conversation ( + conv uuid PRIMARY KEY, + access set, + access_role int, + access_roles_v2 set, + creator uuid, + deleted boolean, + epoch bigint, + group_id blob, + message_timer bigint, + name text, + protocol int, + receipt_mode int, + team uuid, + type int +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.user_team ( + user uuid, + team uuid, + PRIMARY KEY (user, team) +) WITH CLUSTERING ORDER BY (team ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.service ( + provider uuid, + id uuid, + auth_token ascii, + base_url blob, + enabled boolean, + fingerprints set, + PRIMARY KEY (provider, id) +) WITH CLUSTERING ORDER BY (id ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.data_migration ( + id int, + version int, + date timestamp, + descr text, + PRIMARY KEY (id, version) +) WITH CLUSTERING ORDER BY (version ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.team_features ( + team_id uuid PRIMARY KEY, + app_lock_enforce int, + app_lock_inactivity_timeout_secs int, + app_lock_status int, + conference_calling int, + digital_signatures int, + file_sharing int, + file_sharing_lock_status int, + guest_links_lock_status int, + guest_links_status int, + legalhold_status int, + search_visibility_inbound_status int, + search_visibility_status int, + self_deleting_messages_lock_status int, + self_deleting_messages_status int, + self_deleting_messages_ttl int, + snd_factor_password_challenge_lock_status int, + snd_factor_password_challenge_status int, + sso_status int, + validate_saml_emails int +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.member ( + conv uuid, + user uuid, + conversation_role text, + hidden boolean, + hidden_ref text, + mls_clients set, + otr_archived boolean, + otr_archived_ref text, + otr_muted boolean, + otr_muted_ref text, + otr_muted_status int, + provider uuid, + service uuid, + status int, + PRIMARY KEY (conv, user) +) WITH CLUSTERING ORDER BY (user ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.custom_backend ( + domain text PRIMARY KEY, + config_json_url blob, + webapp_welcome_url blob +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.user_remote_conv ( + user uuid, + conv_remote_domain text, + conv_remote_id uuid, + hidden boolean, + hidden_ref text, + otr_archived boolean, + otr_archived_ref text, + otr_muted_ref text, + otr_muted_status int, + PRIMARY KEY (user, conv_remote_domain, conv_remote_id) +) WITH CLUSTERING ORDER BY (conv_remote_domain ASC, conv_remote_id ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.legalhold_whitelisted ( + team uuid PRIMARY KEY +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.member_remote_user ( + conv uuid, + user_remote_domain text, + user_remote_id uuid, + conversation_role text, + PRIMARY KEY (conv, user_remote_domain, user_remote_id) +) WITH CLUSTERING ORDER BY (user_remote_domain ASC, user_remote_id ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.team_member ( + team uuid, + user uuid, + invited_at timestamp, + invited_by uuid, + legalhold_status int, + perms frozen, + PRIMARY KEY (team, user) +) WITH CLUSTERING ORDER BY (user ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.team_notifications ( + team uuid, + id timeuuid, + payload blob, + PRIMARY KEY (team, id) +) WITH CLUSTERING ORDER BY (id ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.legalhold_pending_prekeys ( + user uuid, + key int, + data text, + PRIMARY KEY (user, key) +) WITH CLUSTERING ORDER BY (key ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.group_id_conv_id ( + group_id blob PRIMARY KEY, + conv_id uuid, + domain text +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.user ( + user uuid, + conv uuid, + PRIMARY KEY (user, conv) +) WITH CLUSTERING ORDER BY (conv ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.legalhold_service ( + team_id uuid PRIMARY KEY, + auth_token ascii, + base_url blob, + fingerprint blob, + pubkey pubkey +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.conversation_codes ( + key ascii, + scope int, + conversation uuid, + value ascii, + PRIMARY KEY (key, scope) +) WITH CLUSTERING ORDER BY (scope ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.clients ( + user uuid PRIMARY KEY, + clients set +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.team_conv ( + team uuid, + conv uuid, + managed boolean, + PRIMARY KEY (team, conv) +) WITH CLUSTERING ORDER BY (conv ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.team ( + team uuid PRIMARY KEY, + binding boolean, + creator uuid, + deleted boolean, + icon text, + icon_key text, + name text, + search_visibility int, + status int +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE galley_test.billing_team_member ( + team uuid, + user uuid, + PRIMARY KEY (team, user) +) WITH CLUSTERING ORDER BY (user ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE KEYSPACE gundeck_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; + +CREATE TABLE gundeck_test.push ( + ptoken text, + app text, + transport int, + client text, + connection blob, + usr uuid, + PRIMARY KEY (ptoken, app, transport) +) WITH CLUSTERING ORDER BY (app ASC, transport ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE gundeck_test.notifications ( + user uuid, + id timeuuid, + clients set, + payload blob, + PRIMARY KEY (user, id) +) WITH CLUSTERING ORDER BY (id ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'tombstone_threshold': '0.1'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 0 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE gundeck_test.meta ( + id int, + version int, + date timestamp, + descr text, + PRIMARY KEY (id, version) +) WITH CLUSTERING ORDER BY (version ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE gundeck_test.user_push ( + usr uuid, + ptoken text, + app text, + transport int, + arn text, + client text, + connection blob, + PRIMARY KEY (usr, ptoken, app, transport) +) WITH CLUSTERING ORDER BY (ptoken ASC, app ASC, transport ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE KEYSPACE brig_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; + +CREATE TYPE brig_test.asset ( + typ int, + key text, + size int +); + +CREATE TYPE brig_test.pubkey ( + typ int, + size int, + pem blob +); + +CREATE TABLE brig_test.team_invitation_info ( + code ascii PRIMARY KEY, + id uuid, + team uuid +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.rich_info ( + user uuid PRIMARY KEY, + json blob +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.user_keys_hash ( + key blob PRIMARY KEY, + key_type int, + user uuid +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.service_tag ( + bucket int, + tag bigint, + name text, + service uuid, + provider uuid, + PRIMARY KEY ((bucket, tag), name, service) +) WITH CLUSTERING ORDER BY (name ASC, service ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.login_codes ( + user uuid PRIMARY KEY, + code text, + retries int, + timeout timestamp +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.unique_claims ( + value text PRIMARY KEY, + claims set +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 0 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.user_cookies ( + user uuid, + expires timestamp, + id bigint, + created timestamp, + label text, + succ_id bigint, + type int, + PRIMARY KEY (user, expires, id) +) WITH CLUSTERING ORDER BY (expires ASC, id ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.mls_key_packages ( + user uuid, + client text, + ref blob, + data blob, + PRIMARY KEY ((user, client), ref) +) WITH CLUSTERING ORDER BY (ref ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.mls_key_package_refs ( + ref blob PRIMARY KEY, + client text, + conv uuid, + conv_domain text, + domain text, + user uuid +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.excluded_phones ( + prefix text PRIMARY KEY, + comment text +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.codes ( + user uuid, + scope int, + code text, + retries int, + PRIMARY KEY (user, scope) +) WITH CLUSTERING ORDER BY (scope ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.user_handle ( + handle text PRIMARY KEY, + user uuid +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.service ( + provider uuid, + id uuid, + assets list>, + auth_tokens list, + base_url blob, + descr text, + enabled boolean, + fingerprints list, + name text, + pubkeys list>, + summary text, + tags set, + PRIMARY KEY (provider, id) +) WITH CLUSTERING ORDER BY (id ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.team_invitation_email ( + email text, + team uuid, + code ascii, + invitation uuid, + PRIMARY KEY (email, team) +) WITH CLUSTERING ORDER BY (team ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.invitation_info ( + code ascii PRIMARY KEY, + id uuid, + inviter uuid +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.service_whitelist ( + team uuid, + provider uuid, + service uuid, + PRIMARY KEY (team, provider, service) +) WITH CLUSTERING ORDER BY (provider ASC, service ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.provider ( + id uuid PRIMARY KEY, + descr text, + email text, + name text, + password blob, + url blob +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.user_keys ( + key text PRIMARY KEY, + user uuid +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.mls_public_keys ( + user uuid, + client text, + sig_scheme text, + key blob, + PRIMARY KEY (user, client, sig_scheme) +) WITH CLUSTERING ORDER BY (client ASC, sig_scheme ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.invitee_info ( + invitee uuid PRIMARY KEY, + conv uuid, + inviter uuid +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.provider_keys ( + key text PRIMARY KEY, + provider uuid +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.service_team ( + provider uuid, + service uuid, + team uuid, + user uuid, + conv uuid, + PRIMARY KEY ((provider, service), team, user) +) WITH CLUSTERING ORDER BY (team ASC, user ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.blacklist ( + key text PRIMARY KEY +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.service_whitelist_rev ( + provider uuid, + service uuid, + team uuid, + PRIMARY KEY ((provider, service), team) +) WITH CLUSTERING ORDER BY (team ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.team_invitation ( + team uuid, + id uuid, + code ascii, + created_at timestamp, + created_by uuid, + email text, + name text, + phone text, + role int, + PRIMARY KEY (team, id) +) WITH CLUSTERING ORDER BY (id ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.user ( + id uuid PRIMARY KEY, + accent list, + accent_id int, + activated boolean, + assets list>, + country ascii, + email text, + email_unvalidated text, + expires timestamp, + feature_conference_calling int, + handle text, + language ascii, + managed_by int, + name text, + password blob, + phone text, + picture list, + provider uuid, + searchable boolean, + service uuid, + sso_id text, + status int, + team uuid +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.properties ( + user uuid, + key ascii, + value blob, + PRIMARY KEY (user, key) +) WITH CLUSTERING ORDER BY (key ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.service_user ( + provider uuid, + service uuid, + user uuid, + conv uuid, + team uuid, + PRIMARY KEY ((provider, service), user) +) WITH CLUSTERING ORDER BY (user ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.prekeys ( + user uuid, + client text, + key int, + data text, + PRIMARY KEY (user, client, key) +) WITH CLUSTERING ORDER BY (client ASC, key ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.password_reset ( + key ascii PRIMARY KEY, + code ascii, + retries int, + timeout timestamp, + user uuid +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.clients ( + user uuid, + client text, + capabilities set, + class int, + cookie text, + ip inet, + label text, + lat double, + lon double, + model text, + tstamp timestamp, + type int, + PRIMARY KEY (user, client) +) WITH CLUSTERING ORDER BY (client ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.budget ( + key text PRIMARY KEY, + budget int +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 0 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.connection_remote ( + left uuid, + right_domain text, + right_user uuid, + conv_domain text, + conv_id uuid, + last_update timestamp, + status int, + PRIMARY KEY (left, right_domain, right_user) +) WITH CLUSTERING ORDER BY (right_domain ASC, right_user ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.users_pending_activation ( + user uuid PRIMARY KEY, + expires_at timestamp +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.connection ( + left uuid, + right uuid, + conv uuid, + last_update timestamp, + message text, + status int, + PRIMARY KEY (left, right) +) WITH CLUSTERING ORDER BY (right ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; +CREATE INDEX conn_status ON brig_test.connection (status); + +CREATE TABLE brig_test.meta ( + id int, + version int, + date timestamp, + descr text, + PRIMARY KEY (id, version) +) WITH CLUSTERING ORDER BY (version ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.invitation ( + inviter uuid, + id uuid, + code ascii, + created_at timestamp, + email text, + name text, + phone text, + PRIMARY KEY (inviter, id) +) WITH CLUSTERING ORDER BY (id ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.activation_keys ( + key ascii PRIMARY KEY, + challenge ascii, + code ascii, + key_text text, + key_type ascii, + retries int, + user uuid +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.vcodes ( + key ascii, + scope int, + account uuid, + email text, + phone text, + retries int, + value ascii, + PRIMARY KEY (key, scope) +) WITH CLUSTERING ORDER BY (scope ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 0 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE brig_test.service_prefix ( + prefix text, + name text, + service uuid, + provider uuid, + PRIMARY KEY (prefix, name, service) +) WITH CLUSTERING ORDER BY (name ASC, service ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE KEYSPACE spar_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; + +CREATE TABLE spar_test.scim_external_ids ( + external text PRIMARY KEY, + user uuid +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.bind_cookie ( + cookie text PRIMARY KEY, + session_owner uuid +) WITH bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.user_v2 ( + issuer text, + normalized_uname_id text, + sso_id text, + uid uuid, + PRIMARY KEY (issuer, normalized_uname_id) +) WITH CLUSTERING ORDER BY (normalized_uname_id ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.data_migration ( + id int, + version int, + date timestamp, + descr text, + PRIMARY KEY (id, version) +) WITH CLUSTERING ORDER BY (version ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.authresp ( + resp text PRIMARY KEY, + end_of_life timestamp +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.idp_raw_metadata ( + id uuid PRIMARY KEY, + metadata text +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.issuer_idp ( + issuer text PRIMARY KEY, + idp uuid +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.idp ( + idp uuid PRIMARY KEY, + api_version int, + extra_public_keys list, + issuer text, + old_issuers list, + public_key blob, + replaced_by uuid, + request_uri text, + team uuid +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.default_idp ( + partition_key_always_default text, + idp uuid, + PRIMARY KEY (partition_key_always_default, idp) +) WITH CLUSTERING ORDER BY (idp ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.team_provisioning_by_team ( + team uuid, + id uuid, + created_at timestamp, + descr text, + idp uuid, + token_ text, + PRIMARY KEY (team, id) +) WITH CLUSTERING ORDER BY (id ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.meta ( + id int, + version int, + date timestamp, + descr text, + PRIMARY KEY (id, version) +) WITH CLUSTERING ORDER BY (version ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.verdict ( + req text PRIMARY KEY, + format_con int, + format_mobile_error text, + format_mobile_success text +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.authreq ( + req text PRIMARY KEY, + end_of_life timestamp +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.team_provisioning_by_token ( + token_ text PRIMARY KEY, + created_at timestamp, + descr text, + id uuid, + idp uuid, + team uuid +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.team_idp ( + team uuid, + idp uuid, + PRIMARY KEY (team, idp) +) WITH CLUSTERING ORDER BY (idp ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.issuer_idp_v2 ( + issuer text, + team uuid, + idp uuid, + PRIMARY KEY (issuer, team) +) WITH CLUSTERING ORDER BY (team ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.scim_user_times ( + uid uuid PRIMARY KEY, + created_at timestamp, + last_updated_at timestamp +) WITH bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.scim_external ( + team uuid, + external_id text, + user uuid, + PRIMARY KEY (team, external_id) +) WITH CLUSTERING ORDER BY (external_id ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + +CREATE TABLE spar_test.user ( + issuer text, + sso_id text, + uid uuid, + PRIMARY KEY (issuer, sso_id) +) WITH CLUSTERING ORDER BY (sso_id ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + diff --git a/changelog.d/4-docs/docs-de-unionise-legacy-docs b/changelog.d/4-docs/docs-de-unionise-legacy-docs new file mode 100644 index 00000000000..d0ac25e3e53 --- /dev/null +++ b/changelog.d/4-docs/docs-de-unionise-legacy-docs @@ -0,0 +1 @@ +Move old /docs to /docs/legacy (leaving references). diff --git a/docs/README.origial.md b/docs/README.origial.md new file mode 120000 index 00000000000..5888983db9f --- /dev/null +++ b/docs/README.origial.md @@ -0,0 +1 @@ +legacy/README.md \ No newline at end of file diff --git a/docs/developer/api-versioning.md b/docs/developer/api-versioning.md index d5ee1892666..741d4dcc232 100644 --- a/docs/developer/api-versioning.md +++ b/docs/developer/api-versioning.md @@ -1,464 +1 @@ -# Status of this document - -This is a proposal for API versioning that we may adopt in the future. -If you don't find any trace of it in the code base, that means you're -not to late to submit a PR yourself! :) - -# Introduction - -Since upgrades cannot happen instantaneously, we need to release wire -backends and wire client apps that work together accross releases. - -In the past, we have made sure that the api is changed in a way that -the backend can be newer than the client, and then releasing the -change on the backend first. This worked well on the cloud where we -had control, but fails a lot in the on-prem setting: if we add a new -end-point to the API, the backend will still be able to handle older -clients that simply don't know about that end-point, but the client -won't handle old backends well, since it will try to call the -end-point, and fail. - -The problem becomes more complicated still if you think about backends -talking to other backends in the context of federation: An HTTP server -will act as a client inside the handler function, and the version it -responds with may differ from the version it talks to another backend -it needs to query. - -The new approach outlined here introduces API versions to address this -problem. Every API version is only compatible with itself, but every -node in the network can support a *set of API versions*. A HTTP -client can query the set of supported versions from an HTTP server, -and then pick one that works for it. - -We believe this is a good approach to solve all our API compatibility -problems both between apps and backends and between backends in the -context of federation, but we also list some open questions (and -probably forget to list more). - -In the following, we will refer to HTTP clients as "clients", no -matter whether it is an app or a backend talking to another backend -(federation); and to HTTP servers as "server", no matter which API is -it serving (federation or app). - - -# Versions and servant routing tables - -All routing tables for which a new version is born will be changed -into taking the version number as a parameter, which is added as a -prefix to every route: - -```haskell -data Api (version :: Symbol) routes = Api - { getUnqualifiedConversation :: - routes - :- version - :> Summary "Get a conversation by ID" - :> ZLocalUser - :> "conversations" - :> Capture "cnv" ConvId - :> Get '[Servant.JSON] Conversation, - getConversation :: - routes - :- version - :> Summary "Get a conversation by ID" - :> ZLocalUser - :> "conversations" - :> QualifiedCapture "cnv" ConvId - :> Get '[Servant.JSON] Conversation, - [...] - } -``` - -APIs of all the supported versions can be composed like this: - -```haskell -type ServantAPI = - ToServantApi (Api "v1") - :<|> ToServantApi (Api "v2") - :<|> ToServantApi (Api "v4") -- v3 is broken -``` - -This will result in a routing table like this one: - -``` -/v1/users/... -/v1/conversations/... -/v2/users/... -/v2/conversations/... -/v4/users/... -/v4/conversations/... -``` - - -## Changes between versions - -The point of having versions is of course not that all of them look -exactly the same except for their prefix. The point is that there are -other things that change between versions. - -There are essentially two categories of changes: - -1. **data**: request or response bodies, variable path segments, - possible headers or status codes (as in `UVerb` or `MultiVerb`), - etc. -2. **structure**: literal path segments, verb, the version itself, ... - - -## Changes in the data - -If a data type in request, response, variable path segments, or anywhere else -changes, introduce a type family parameterized in the version. - -```haskell -[...] - getConversation :: - routes - :- version - :> Summary "Get a conversation by ID" - :> ZLocalUser - :> "conversations" - :> QualifiedCapture "cnv" ConvId - :> Get '[Servant.JSON] (ConversationV version), -[...] - -type family ConversationV (version :: Symbol) :: * where - ConversationV "v1" = Conversation - ConversationV "v2" = Conversation - ConversationV "v4" = ConversationV4 -``` - -Note that before version `"v4"`, nothing changed for this type, so -there was no need to introduce a new concrete data type. - -If the last change of a data type is entirely phased out, the type -family turns constant and can be removed again. If you see this in -your code: - -```haskell -type family ConversationV (version :: Symbol) :: * where - ConversationV "v4" = ConversationV4 - ConversationV "v5" = ConversationV4 - ConversationV "v6" = ConversationV4 - ConversationV "v7" = ConversationV4 -``` - -You can remove 'ConversationV', rename `ConversationV4` to Conversation, -and use it in the routing table instead of `ConversationV` again, as -before `"v4"`. - -### Open questions - -Adding/removing a new version now requires to touch a lot of type -families. The changes are trivial and the compiler will lead us to -all of them, but there are potentially many. - -It would be nice to have a default type that is used for all versions -that don't explicitly mention a given type, then future versions would -only have to be touched if a type actually changes. However, (a) it's -not clear to us how to accomplish that (open type families plus -multi-param type classes plus overlapping instances? type-level -'Maybe' with type-level default type?); and (b) it is less robust and -transparent, and depending on the solution carries the risk of missing -a spot where we want to update a type, but the compiler picks the -wrong default. - - -## Changing structure - -Without loss of generality, we only consider additions and deletions -of routes in this section: if you want to change the path or verb of -an end-point, add a new path instead, and phase the old one out (now -or in some future version). - -When end-points are present in some supported versions, but not in -others, their record fields in the servant routing type needs to be -present for all versions, but in some versions should behave as if it -weren't. - -This is best solved by a new data type: - -```haskell -data NotInThisVersion = NotInThisVersion - -- (The value constructor may be needed to implement the handler) - -- (Or something involving `Verb 'NOTINTHISVERSION`?) -``` - -The entire route will then be a type family over the version that maps -all unsupported versions to `NotInThisVersion`. - -Now we can write a type family that can crawl a `ServantAPI` (not the -record one, the one with `:<|>`) and drop all the routes marked as not -existing. - -This will save us the trouble of writing lots of instances for -`NotInThisVersion` (server, swagger, client, ...), and yield exactly -the desired result: - -```haskell -type ServantAPI = - DropNotInThisVersion - ( ToServantApi (Api "v1") - :<|> ToServantApi (Api "v2") - :<|> ToServantApi (Api "v4") -- v3 is broken - ) -``` - - -## Adoption of versioned APIs - -When API versions are introduced to a code base that has a routing -table without versions, the question arises what to do with old -clients or servers talking to new ones. - -We define a middleware that - - (1) maps requests without version prefix in the path to ones that - have version `"v0"`. - - (2) responds with a specific type of error if an unsupported version - is requested (so the client can re-negotiate a new version to - speak after an upgrade, see below). - - -## Version handshake - -Client and server need to agree on a version to use. The server -provides two (kinds of) end-points for that. - -``` -GET /api-versions -=> { "supported": [1, 2, 45, 119] } - -GET /v*/api-docs -=> -``` - -The client developer can pull the swagger docs of a new version and -diff it against the one they already support, and work their way -through the changes (see below). - -The client will call `GET /api-versions` and pick any number in the -intersection of the versions it supports and the ones the server -supports (usually the largest). - -*Corner case:* if we want to distinguish between backend-to-backend -and client-to-backend, we can do that in path suffixes (`GET -/api-versions/{client,federation}` etc.). - - -### No shared api version - -If the intersection set is empty, the client has no way of talking to -the server. It needs to politely fail with an error message about -upgrading, downgrading, or talking to another server. - -This should only happen if the distance between last upgrade on client -and server exceeds the agreed-upon limits (eg., 6 months). - - -### Update detection and version re-negotation - -If the server is upgraded and some old supported versions are phased -out, the client may be caught by surprise. - -Servers should respond with status `404`, error label -`unsupported-version`. The versioning middleware can do that (see -above). - -Client should install a catch-all that will handle this specific -error, re-fetch `/api-versions`, and try again. - -This will only happen if backend and client grow apart in time beyond -the supported limit, and there is some chance it will result in an -empty set of compatible versions, so it's also ok to just fail here. - - -## Strongly typed versions - -If we make version an ADT `ApiVersion`, we can remove old versions -from it in one place and have the compiler guide us through all the -places where we need to remove it. - -There are at least two ways to implement this: - -1. Add a few extra servant instances for `(v :: ApiVersion) :> route`. -2. Define a type family `Versions` that maps `V*` to `"v*"`, and write -`Versions version :>` in the routing type instead of `"v1"`. - -2 seems a lot less work to write, read, and understand. - - -## Data migration (aka data marshalling) - -If the shape of an end-point changes between versions (if a data type -in the routing table becomes a type family), it is often possible to -write marshalling functions that translate a value from an older -version into one of a newer version or vice versa. - -These functions are called marshalling functions and are useful to -define separately to keep the application logic clean. - -For certain changes to a data type used in an API, marshalling is -straight-forward in both directions. The most common example is -adding an optional attribute to a JSON object: - -- *backward migration*: remove the new attribute. -- *forward migration*: set the new attribute to `null`. - -(This is what wire has traditionally done to accomplish client -backwards compatibility without any API versioning.) - -If a mandatory attribute is added in a newer version, there may be a -plausible default value that can be used in the forward migration -(backward migration would still remove the field). - -In other cases, whether there is an automatic migration depends on the -use case and the semantics. - -It may even be impossible to marshal either in one or in both -directions. In this case, you have 3 options: - -1. abandon compatibility; -2. rethink your new version and craft it in a way that two-way - marshalling is possible; -3. make the application work around the gap, eg. by gracefully - refusing to offer video conferencing in a client if it is not - supported on the server yet. - - -## Writing client code - -If you write all code by hand and don't generate anything from the -swagger docs, just look at the swagger diff for every new version and -take it from there. - -If you generate, say, typescript or kotlin or swift from swagger: - -0. have a generated source module `Gen.ts`, plus a source module with - manually written code `Man.ts`. Re-export everything from `Gen.ts` - in `Man.ts`, and only import `Man.ts` in any modules that contain - application logic. - -1. look at the diff of the swagger of the last supported and the new - versions. - -2. copy all functions for routes that have changed from `Gen.ts` to - `Man.ts`. these are speaking an old api version and won't need to - be re-generated any more. work the last version supported by this - function into the name somehow (eg., suffix `"_v23"`). - -3. for every function that moved to `Man.ts` in this way, write a - function *without* the version suffix. It somehow knows the api - version of the server that it talks to (function parameter, app - config, dosn't matter), and decides based on that whether to call - the deprecated function with the `"_v23"` suffix or the one from - `Gen.ts`. If the old one is called, it may have to do some - marshalling of request and response (see above). - -It will happen that a new client will not be able to accomplish -something with an old API. (Example: if video calling is introduced in -`"v12"`, you can't emulate `POST /video-call` when talking to a `"v9"` -server. In these cases, the function in `Man.ts` must raise a "server -too old" exception, and gracefully shut down the new functionality.) - - -### Open questions - -It will be interesting to develop a good flow for doing this given the -concrete tooling that's in place: Should every API version get its own -library, or just a source module? Where is the diff to be applied -exactly? On the swagger? Or on the generated code? How do we make -sure the old versions replaced with new versions that have not changed -aren't in the way? - -For the user apps, the task here is to write (or construct) query -function that picks a version and either succeeds or fails. In the -context of federation, the task is to write handlers for all version -(or a handler that can handle all versions). Inside this handler, -when calling other backends as a client, it is not guaranteed that -those backends can be called in the version the handler is operating -under. - -In the simple cases, this can (probably?) be solved by writing -version-oblivious query functions that intially are just aliases for -the generated client functions, but are manually adjusted to being -able to call other servers on all the supported versions. But it will -be interesting to see where the hard cases happen, and if we'll see -them when they do. - - -## Concerns and design alternatives - -### Why not version every end-point separately? - -Yes, that would work in principle. On the backend, it would make the -entire routing table smaller (no need to concatenate the same -end-point for many versions), which may result in shorter compile -times. On the clients, with a new API version it would be -straight-forward to see which end-points need to be worked on, and -which remain unchanged. - -On the other hand, the routing table size may not be an issue, and if -it is there are solutions (introduce a CCP switch to compile only the -most recent API version that you're working on); and the client -process is already quite straight-forward with the approach outlined -above via diffing the swagger docs between most recent version and -predecessor. - -Plus, if the entire API has one version, you get a few advantages: - -1. The fact that clients are forced to commit to a concrete API - version for all end-points when talking to the backend reduces - testing complexity. If there is a mapping of end-points to - versions, the behavior of interacting parties is much less - restricted, and versions that have not been tested against each - other may be used together. (This can be avoided, but it's less - obvious how to get it right, and testing complexity will likely be - worse.) - -2. The "one version" approach makes it obvious which end-points are in - the most recent API at any given point in time. The "one version - per end-point" approach would either yeild a noisy union of all - supported versions, or there would have to be a mechanism for - reconstructing something close to what we get for free otherwise. - -3. The backend code is a good combination of concise and type-safe in - the "one version" approach. If every end-point had its own - version, the routing table entry would either have to accept a - variable path segment for the version, and fail at run-time if the - version is not supported, or you would have to add one handler per - supported version (even if in the case where all versions call the - same handler function with slightly different parameters). - - -### Syntactical vs. behavioral changes - -It is quite common that behavior of end-points changes together with -the syntax, or even without a change in the syntax. - -This is not a fundamental problem: since the handler can be called -with the version as a type parameter, there is no reason why it -shouldn't change behavior with or without changing the syntax. In -each such case, it needs to be decided whether the difference is -significant enough to justify a new API version. - -At the very least though it should result in diverging swagger docs -that explains those differences. - - -### Client capabilities - -Wire supports client capabilities to decide whether a client should be -allowed to use certain parts of the API. - -This is another alternative to API versions, and it is in some ways -more straight-forward to decide who to interpret capability sets. But -this approach has its own problems: Most importantly, the number of -supported capability sets grows quadratically (not in practice, -because historically clients will only ever support a small part of -all possible combinations of capabilities, but that makes thigns -worse: it makes the system more complex, and then doesn't use that -complexity for anything). - -Therefore, the capabilities we're using in the wire code base should -be gracefully phased out and replaced by API versions. +file has moved [here](../legacy/developer/api-versioning.md) diff --git a/docs/developer/architecture/wire-arch-2.png b/docs/developer/architecture/wire-arch-2.png index c991a43f631..55b2ed6ab4b 100644 Binary files a/docs/developer/architecture/wire-arch-2.png and b/docs/developer/architecture/wire-arch-2.png differ diff --git a/docs/developer/architecture/wire-arch-2.xml b/docs/developer/architecture/wire-arch-2.xml index 47b155426de..ace4d60fbcc 100644 --- a/docs/developer/architecture/wire-arch-2.xml +++ b/docs/developer/architecture/wire-arch-2.xml @@ -1 +1 @@ -7Vxbc5s4FP41fqwHENfHxkm7nWky6XpnNnnqyKBgWow8ICf2/vqVQGCEZAfHYFLHyUOQEAK+79x0dMgITBbrrylczm9xgOKRoQXrEbgeGYZuWRr9w3o2RY+hGbwnTKOAj9p2TKP/EO8sh62iAGXCQIJxTKKl2OnjJEE+EfpgmuIXcdgTjsW7LmGIpI6pD2O5998oIPOi1zWcbf9fKArn5Z112yvOzKD/O0zxKuH3GxngKf8pTi9gORd/0WwOA/xS6wI3IzBJMSbF0WI9QTEDt4StuO7LjrPVc6coIW0uMIsLnmG8QuUT589FNiUWL/OIoOkS+qz9Qgkfgas5WcS0pdPDjKT4d4US63mK4niCY5zm15fvT/txQjjVuknbAczmKBCmqV2m5T/0jPxK/C2fUUrQutbFX/ErwgtE0g0dwuXPtPklL1sydbOU0nmdSZt3Qi5BYTXZFkV6wIFUgwo+BqhaS0xNvQtMvT5AbYBXBznBCXoVT/Oa/R6JJz9rmvyNOL66Z0kAA2Cr8DWPx9eS0EQBNYm8iVMyxyFOYHyz7b3K7VwOjiYijdYReagdP7IhY4u1EvpcD2LzkU+QEZiSz8x6b+HP+75E7LHzMSgJyhF+DLMs8otOPoTd8BciZMNZhSuCadf28b9jvNxB/k4KM7xK/dIxlN4IpiHiwwDHjgG2l+gUxZBEz6KPOYY0+yjS9LakDUzMbiu2T5ryxvVaaG14681U//r5w4ofbGNlZcDV73483DnPn0x7KP6dPpRW+xBKqxtDkeb2obQfhDRzKNK8o0nz8YJBuFPrCpZKChkh5dJAYmzLR500XSSN07plTDuAsd0m9yguu+ONX3qPI3rbKoSyNU+8pBAePqrBdnXbVgJQxrCnkgC9ocI0OFQ5uHuURvQtUPqq0PSv5n+q0JQ3asbdPQhRuSg420WMZZkCmGVepL5I1BVrRK8DG1269PNFl86mFlVhDa7L8HayRNRBL+GmZPbea3TStyGyNJFdYDe8WWEdJUN0+EQdWrReEojvSefKjFalcvYpVc6S4L1ZU29PJViCmb4PUSHbQE4BJoyjMGGaRFFikcQVQyfyYfyZn1hEQZBrs4rKGluGlms8oVqA2XWfvI5yjRaQE7imwo0YHWQadVvC/FvyATE3gHM6zB0J8wm16jAJUjgy7JiBPEvpUciO/kZBlElcCKFvZR+0A+mh1sPS2C/tj+EMxfc4izi0ElffGwNmmBC8UJBJmLdhwfmSPexiHbJ9szF+eop8NA4ggTOYoaw6OsQrtbdkricmmAGQgzPgKiwZ6IBgVyJ4meL15pxZ/OmvZioq8zZ/Y8WG3cG8eo5AazVDXW8dmVbL6IBWeVfGp7EFnrMN1wu1xwf8rrg2rfToBNwa/WU8yrzGJ22sGVVPI3fZPrXRKh/2zrIbVVlDLb1Zxs4dLzQoAnBTG7BkcX8micJhgb8hpzJmaRRelL6LFYc7nNLLOZQQxjG6OOoOiLXtAYmVi1JCZqr93xdmOwit3QazJ4zBFDVce9ZON9QNUkSnCKb+/Jyp72cNZZVJ0AHWUIacDKoRfWHyQGPsDMiknGJ6xKt/mK2SFHa6xCR62ihOsFB7EuNVIJ/7inFIJcHQbuHyjDIlPnvbbLxMo2dIqHHPX74X6fBE4TBdudjRUiR9QQe5MEPOhU3p09DrtJztLynmafsLp4ctph1nOFLl/Nf0ZlqwOv3BD643CVzg6ytZn+/QeoHl7n9eojjCF1l4yxrLGk4W5KTZ9K4uCxc6D4+/7Qad8i5dX3SWc9ToTMIoWXfIYxPfReZDNM5Jo+BmaBxSfF/gRqa82gltgv52EXh156qQiS6U1BX3sctmnVTFhpTVBalyesuHCaXjfLRzuNWyrjnDJUJAL1UsYtG8IRbgjoQEt9G2pL7X8tryowXhQ4bBymvB2ZeR6BpoFpKoarcMWehN3eoAYDl3cGYAu+ag+H70opHyrKmJNFTe+ARVJEBeOX+saqldJJyyfArIK13Z4UqfKdR4EEwGa9xDwkgsgg/NHElbwW03Zvd+LrazHLWdO/UU7tRr6U5bxrPHlpe64pIXeA2225aX2qAxkdmYqLvyUlMhO70Ea8PHY22F5Ugp8ICYBAPNT2baSkHT3xqe25sUtIgOlZUeqpqTRrlJzv5Oht70xeBp1Ln5ubiEf2t1Fnm0+9PmvV/PvuvIVPK9CmnZ7Y4NKfRX1ZD3FZqqUT/uK9Rz+N7/9Q/EG5r+qoUoVrGdm2zbNseix9XLGvVDdd3T9eZUXmOqHeq+u4hMfSdd13TlM2+ltpiyW2PS4r+EtPcUY02zRW8BNOcVyVGWLpb6weodC9mrlAQA3tzOxxqbWqM5W3vhrAcYe42vHLYO5dYkYylFl3+IX7PkbYFH+uS0Z/r59jv98y24z/d8mpt6Gz5q8u2WsYpQKo9R9TTE/I/NSx+0a3SkY3as8XD7gGqxkev0MvpG50PvUdsOx9LtNOg+ZZ2emu69xXvny3lfCu2Ki6Iei7poc/tv+gqnsf1niODmfw== \ No newline at end of file +file has moved [here](../legacy/developer/architecture/wire-arch-2.xml) diff --git a/docs/developer/cassandra-interaction.md b/docs/developer/cassandra-interaction.md index 8cc9d3ee514..63af6207cd7 100644 --- a/docs/developer/cassandra-interaction.md +++ b/docs/developer/cassandra-interaction.md @@ -1,98 +1 @@ -# Writing code interacting with cassandra - - - -* [Anti-patterns](#anti-patterns) - * [Anti-pattern: Using full table scans in production code](#anti-pattern-using-full-table-scans-in-production-code) - * [Anti-pattern: Using IN queries on a field in the partition key](#anti-pattern-using-in-queries-on-a-field-in-the-partition-key) - * [Anti-pattern: Designing for a lot of deletes or updates](#anti-pattern-designing-for-a-lot-of-deletes-or-updates) -* [Understanding more about cassandra](#understanding-more-about-cassandra) - * [primary partition clustering keys](#primary-partition-clustering-keys) - * [optimizing parallel request performance](#optimizing-parallel-request-performance) -* [Cassandra schema migrations](#cassandra-schema-migrations) - * [Backwards compatible schema changes](#backwards-compatible-schema-changes) - * [Backwards incompatible schema changes](#backwards-incompatible-schema-changes) - * [What to do about backwards incompatible schema changes](#what-to-do-about-backwards-incompatible-schema-changes) - - - -## Anti-patterns - -### Anti-pattern: Using full table scans in production code - -Queries such as `select some_field from some_table;` are full table scans. Cassandra is not optimized at all for such queries, and even with a small amount of data, a single such query can completely mess up your whole cluster performance. We had an example of that which made our staging environment unusable. Luckily, it was caught in time and [fixed](https://github.com/wireapp/wire-server/pull/1574/files) before making its way to production. - -Suggested alternative: Design your tables in a way to make use of a primary key, and always make use of a `WHERE` clause: `SELECT some_field FROM some_table WHERE some_key = ?`. - -In some rare circumstances you might not easily think of a good primary key. In this case, you could for instance use a single default value that is hardcoded: `SELECT some_field FROM some_table WHERE some_key = 1`. We use this strategy in the `meta` table which stores the cassandra version migration information, and we use it for a [default idp](https://github.com/wireapp/wire-server/blob/4814afd88b8c832c4bd8c24674886c5d295aff78/services/spar/schema/src/V7.hs). `some_field` might be of type `set`, which allows you to have some guarantees. See the implementation of unique claims and the [note on guarantees of CQL -sets](https://github.com/wireapp/wire-server/blob/develop/services/brig/src/Brig/Unique.hs#L110) for more information on sets. - -### Anti-pattern: Using IN queries on a field in the partition key - -Larger IN queries lead to performance problems. See https://lostechies.com/ryansvihla/2014/09/22/cassandra-query-patterns-not-using-the-in-query-for-multiple-partitions/ - -A preferred way to do this lookup here is to use queries operating on single keys, and make concurrent requests. One way to do this is with the [`pooledMapConcurrentlyN`] (https://hoogle.zinfra.io/file/root/.stack/snapshots/x86_64-linux/e2cc9ab01ac828ffb6fe45a45d38d7ca6e672fb9fe95528498b990da673c5071/8.8.4/doc/unliftio-0.2.13/UnliftIO-Async.html#v:pooledMapConcurrentlyN) function. To be conservative, you can use N=8 or N=32, we've done this in other places and not seen problematic performance -yet. For an optimization of N, see the section further below. - -### Anti-pattern: Designing for a lot of deletes or updates - -Cassandra works best for write-once read-many scenarios. - -Read e.g. -- https://www.datastax.com/blog/cassandra-anti-patterns-queues-and-queue-datasets -- https://www.instaclustr.com/support/documentation/cassandra/using-cassandra/managing-tombstones-in-cassandra/# -- search the internet some more for 'cassandra' and 'tombstones' and if you find good posts, add them here. - -## Understanding more about cassandra - -### primary partition clustering keys - -Confused about primary key, partition key, and clustering key? See e.g. [this post](https://blog.devgenius.io/cassandra-primary-vs-partitioning-vs-clustering-keys-3b3fa0e317f4) or [this one](https://dzone.com/articles/cassandra-data-modeling-primary-clustering-partiti) - -### optimizing parallel request performance - -See the thoughts in https://github.com/wireapp/wire-server/pull/1345#discussion_r567829234 - measuring overall and per-request performance and trying out different settings here might be worthwhile if increasing read or write performance is critical. - -## Cassandra schema migrations - -### Backwards compatible schema changes - -Most cassandra schema changes are backwards compatible, or *should* be designed to be so. Looking at the changes under `services/{brig,spar,galley,gundeck}/schema` you'll find this to be mostly the case. - -The general deployment setup for services interacting with cassandra have the following assumption: - -* cassandra schema updates happen *before* new code is deployed. - * This is safeguarded by the concourse deployment pipelines - * This is also safeguarded by the `wire-server` helm chart, which deploys the `cassandra-migrations` job as part of a helm [pre-install/pre-upgrade hook](https://github.com/wireapp/wire-server/blob/b3b1af6757194aa1dc86a8f387887936f2afd2fb/charts/cassandra-migrations/templates/migrate-schema.yaml#L10-L13): that means the schema changes are applied, and helm waits before launching the new code in the brig/galley/spar/gundeck pods until the changes have completed applying. - * This is further safeguarded by the code e.g. in brig refusing to even start the service up if the applied schema migration is not at least at a version the code expects it to. See [versionCheck](https://github.com/wireapp/wire-server/blob/b3b1af6757194aa1dc86a8f387887936f2afd2fb/services/brig/src/Brig/App.hs#L411) and [schemaVersion](https://github.com/wireapp/wire-server/blob/b3b1af6757194aa1dc86a8f387887936f2afd2fb/services/brig/src/Brig/App.hs#L136-L137) - -So usually with these safeguards in place, and backwards-compatible changes, we have the following: - -* At time t=0, old schema, old code serves traffic; all good. -* At time t=1, new schema, old code serves traffic: all good since backwards compatible. -* At time t=2, new schema, old code AND new code serve traffic: all good since backwards compatible. -* At time t=3, new schema, new code serves traffic: all good! - -If this order (apply schema first; then deploy code) is not safeguarded, then there will be code running in e.g. production which `SELECT my_new_field FROM my_new_table` even though this doesn't yet exist, leading to 500 server errors for as long as the mismatch between applied schema and code version persists. - -### Backwards incompatible schema changes - -In the case where a schema migration is **not backwards compatible**, such as in the form of `ALTER TABLE my_table DROP my_column`, the reverse problem exists: - -During a deployment: - -* At time t=0, old schema, old code serves traffic; all good. -* At time t=1, new schema, old code serves traffic: 500 server errors as the old code is still active, bit columns or tables have been deleted, so old queries of `SELECT x from my_table` cause exceptions / HTTP 5xx results. - * In the worst deployment scenario, this could raise an alarm and lead operators or automation to stop the deployment or roll back; but that doesn't solve the issue, 500s will continue being thrown until the new code is deployed. -* At time t=2, new schema, old code AND new code serve traffic: partial 500s (all traffic still serves by old code) -* At time t=3, new schema, new code serves traffic: all good again. - -#### What to do about backwards incompatible schema changes - -Options from most to least desirable: - -* Never make backwards-incompatbile changes to the database schema :) -* Do changes in a two-step process: - * First make a change that stops the code from using queries involving `my_column` or `my_table` (assuming you wish to remove those), and deploy this all the way across all of your environments (staging, prod, customers, ...). - * After all deployments have gone through (this can take weeks or months), do the schema change removing that column or table from the database. Often there is no urgency with removal of data, so it's fine to do this e.g. 6 months later. -* Do changes in a one-step process, but accept partial service interruption for several minutes up to a few hours, accept communication overhead across teams to warn them about upcoming interruption or explain previous interruption; accept communication and documentation to warn all operators deploying that or a future version of the code, and accept some amount of frustration that may arise from a lack of said communication and understanding of what is happening. +file has moved [here](../legacy/developer/cassandra-interaction.md) diff --git a/docs/developer/changelog.md b/docs/developer/changelog.md index 886df37964a..508079c863b 100644 --- a/docs/developer/changelog.md +++ b/docs/developer/changelog.md @@ -1,41 +1 @@ -The wire-server repo has a process for changelog editing that prevents -merge conflicts and enforces a consistent structure to the release -notes. - -*Introduced in https://github.com/wireapp/wire-server/pull/1749.* - -## tl;dr - -Entries have to be written in individual files in a relevant subfolder of `./changelog.d/`. - -*Example*: create the file `./changelog.d/2-features/potato-peeler` with one-line contents `Introduce automatic potato peeler functionality when buying potatoes, see [docs](link-to-docs)` - -## Details - -On every pull request, one is supposed to create a new file in the -appropriate subdirectory of `changelog.d`, containing just the text of -the corresponding changelog entry. There is no need to explicitly -write a PR number, because the `mk-changelog.sh` script will add it -automatically at the end. The name of the file does not matter, but -please try to make it unique to avoid unnecessary conflicts (e.g. use -the branch name). - -It is still possible to write the PR number manually if so desired, -which is useful in case the entry should refer to multiple PRs. In -that case, the script leaves the PR number reference intact, as long -as it is at the very end of the entry (no period allowed afterwards!), -and in brackets. It is also possible to use the pattern `##` to refer -to the current PR number. This will be replaced throughout. - -Multiline entries are supported, and should be handled -correctly. Again, the PR reference should either be omitted or put at -the very end. If multiple entries for a single PR are desired, one -should create a different file for each of them. - -## Generating a CHANGELOG for a release - -Just run the script `changelog.d/mk-changelog.sh` with no -arguments. It will print all the entries, nicely formatted, on -standard output. The script gets PR numbers from the `git` log. If -that fails for any reason (e.g. if an entry was added outside of a -PR), make sure that the entry has a manually specified PR number. +file has moved [here](../legacy/developer/changelog.md) diff --git a/docs/developer/dependencies.md b/docs/developer/dependencies.md index 0d5d01619f3..392d1d14c90 100644 --- a/docs/developer/dependencies.md +++ b/docs/developer/dependencies.md @@ -1,248 +1 @@ -# Dependencies {#DevDeps} - -This page documents how to install necessary dependencies to work with the wire-server code base. - -This repository makes use of git submodules. When cloning or updating, use `git submodule update --init --recursive` to check out the code dependencies. - -## General package dependencies (needed to compile Haskell services) - -*Note: all the below sections for getting compile-time dependencies necessary to compile all of wire-server may potentially go out of date; if you spot a mistake please open an issue or PR* - -### Nix + Direnv - -Using Stack's [Nix integration](https://docs.haskellstack.org/en/stable/nix_integration/), Stack will take care of installing any system -dependencies automatically - including `cryptobox-c`. If new system dependencies are needed, add them to the `stack-deps.nix` file in the project root. - -If you have `direnv` and `nix`, you will automatically have `make`, `docker-compose` and `stack` in `PATH` once you `cd` into the project root and `direnv allow`. -You can then run all the builds, and the native dependencies will be automatically present. - -1. Install [Nix](https://nixos.org/download.html) - * MacOS users with a recent Mac might need to follow [these - instructions](https://nixos.org/nix/manual/#sect-macos-installation) - * Debian users can use their distro's `nix` package, and should remember - - to add their user to the `nix-users` group in /etc/group, and re-start - their login session. -2. Install [Direnv](https://direnv.net/). - * On debian, you can install the `direnv` package. On MacOS use `brew install direnv`. - * On NixOS with home-manager, you can set `programs.direnv.enable = true;`. - * Make sure direnv is hooked into your shell via it's appripriate `rc` file. - Add `eval "$(direnv hook bash|zsh|fish)"` to your ~/.(bash|zsh|fish)rc . - * When successfully installed and hooked, direnv should ask you to `direnv allow` - the current `.envrc` when you cd to this repository. - See the [Installation documentation](https://direnv.net/docs/installation.html) for further details. - -### Fedora: - -```bash -sudo dnf install -y pkgconfig haskell-platform libstdc++-devel libstdc++-static gcc-c++ libtool automake openssl-devel libsodium-devel ncurses-compat-libs libicu-devel GeoIP-devel libxml2-devel snappy-devel protobuf-compiler -``` - -### Ubuntu / Debian: - -_Note_: Debian is not recommended due to this issue when running local integration tests: [#327](https://github.com/wireapp/wire-server/issues/327). This issue does not occur with Ubuntu. - -```bash -sudo apt install pkg-config libsodium-dev openssl-dev libtool automake build-essential libicu-dev libsnappy-dev libgeoip-dev protobuf-compiler libxml2-dev zlib1g-dev libtinfo-dev liblzma-dev libpcre3 libpcre3-dev -y -``` - -If `openssl-dev` does not work for you, try `libssl-dev`. - -### Arch: - -``` -# You might also need 'sudo pacman -S base-devel' if you haven't -# installed the base-devel group already. -sudo pacman -S geoip snappy icu openssl ncurses-compat-libs -``` - -### macOS: - -```bash -brew install pkg-config libsodium openssl automake icu4c geoip snappy protobuf -``` - -_Note_: macOS users will need to make sure to link Haskell services against a more recent version of OpenSSL than what ships with the OS by default. Additionally, `icu4c` is installed in a non-standard location by `homebrew`. Add the following to your `.stack/config.yaml`: - -```yaml -extra-include-dirs: -- /usr/local/opt/openssl/include -- /usr/local/opt/icu4c/include - -extra-lib-dirs: -- /usr/local/opt/openssl/lib -- /usr/local/opt/icu4c/lib -``` - -_Note_: if you're getting `fatal error: 'libxml/parser.h' file not found` and you're on macOS Mojave, try doing: - -```bash -sudo installer -pkg /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg -target / -``` - -## Haskell Stack - -Please refer to [Stack's installation instructions](https://docs.haskellstack.org/en/stable/README/#how-to-install). - -When you're done, ensure `stack --version` is the same as `STACK_VERSION` in [`build/ubuntu/Dockerfile.prebuilder`](../../build/ubuntu/Dockerfile.prebuilder). - -If you have to, you can downgrade stack with this command: - -```bash -stack upgrade --binary-version -``` - -### Ubuntu / Debian -_Note_: The packaged versions of `haskell-stack` are too old. It is recommended to follow the generic instructions or to use stack to update stack (`stack upgrade`). - -```bash -sudo apt install haskell-stack -y -``` - -### Generic - -```bash -curl -sSL https://get.haskellstack.org/ | sh -# or -wget -qO- https://get.haskellstack.org/ | sh -``` - -## Rust - -### Ubuntu / Debian -```bash -sudo apt install rustc cargo -y -``` - -### Generic - -```bash -curl https://sh.rustup.rs -sSf | sh -source $HOME/.cargo/env -``` - -## Formatting Haskell files - -You need [`ormolu`](https://github.com/tweag/ormolu) on your PATH, get it with `stack install ormolu` - -## Generating license headers - -We use [`headroom`](https://github.com/vaclavsvejcar/headroom), get it with `stack install headroom` - -## makedeb - -This is a tool to create debian-style binary packages. It is optional, and is only used if you want to install debian-style packages on your debian or ubuntu system. - -_Note_: If you want to build debian-style packages of cryptobox-c and other wire utilities, execute this step. otherwise, make sure to execute the 'Generic' version of the cryptobox-c step. - -```bash -git clone https://github.com/wireapp/wire-server && cd wire-server/tools/makedeb -export VERSION=0 -make dist -dpkg -i ../../dist/makedeb*.deb -``` - -## cryptobox-c - -### Ubuntu / Debian - -```bash -git clone https://github.com/wireapp/cryptobox-c && cd cryptobox-c -make dist -dpkg -i target/release/cryptobox*.deb -``` - -### Generic -```bash -export TARGET_LIB="$HOME/.wire-dev/lib" -export TARGET_INCLUDE="$HOME/.wire-dev/include" -mkdir -p "$TARGET_LIB" -mkdir -p "$TARGET_INCLUDE" -git clone https://github.com/wireapp/cryptobox-c && cd cryptobox-c -make install - -# Add cryptobox-c to ldconfig -sudo bash -c "echo \"${TARGET_LIB}\" > /etc/ld.so.conf.d/cryptobox.conf" -sudo ldconfig -``` - -Make sure stack knows where to find it. In `~/.stack/config.yaml` add: - -(using `~` or `$HOME` doesn't work, needs full paths) - -```yaml -extra-include-dirs: -- /usr/local/include -- /.wire-dev/include - -extra-lib-dirs: -- /usr/local/lib -- /.wire-dev/lib -``` - -## Docker - -_Note_: While it is possible to use non-docker solutions to set up and configure this software, we recommend using docker and our provided docker images to configure dependent services rapidly, and ensure a consistent environment for all potential developers. - -### Ubuntu / Debian Testing/Unstable: -```bash -sudo apt install docker.io docker-compose -``` - -After installing docker-io, add your user to the docker group, and restart your shell (usually involving a restart of your graphical environment). - -once you've logged in again, if you would like to upload any docker images (optional): -```bash -docker login --username= -``` - -### Generic: - -* [Install docker](https://docker.com) -* [Install docker-compose](https://docs.docker.com/compose/install/) - -## Telepresence - -You can instead use [telepresence](https://www.telepresence.io) to allow you to talk to services installed in a given kubernetes namespace on a local or remote kubernetes cluster using easy DNS names like: `curl http://elasticsearch:9200`. - -Requirements: - -* install telepresence (e.g. `nix-env -iA nixpkgs.telepresence`) -* you need access to a kubernetes cluster -* you need a namespace in which you have installed something (e.g. `make kube-integration-setup` will do this) - -### Telepresence example usage: - -``` -# terminal 1 -telepresence --namespace "$NAMESPACE" --also-proxy cassandra-ephemeral -``` - -``` -# terminal 2 -curl http://elasticsearch-ephemeral:9200 -``` - -### Telepresence example usage 2: - -``` -# just one terminal -telepresence --namespace "$NAMESPACE" --also-proxy cassandra-ephemeral --run bash -c "curl http://elasticsearch-ephemeral:9200" -``` - -### Telepresence usage discussion: - -* If you have `fake-aws` and `databases-ephemeral` helm charts set up, you can run either `brig` and other services locally (they connect to cassandra-inside-kubernetes) -* If you also have `brig` and other haskell services running in kubernetes (e.g. you ran `make kube-integration-setup`, you can use telepresence to only run test executables (like `brig-integration`) locally which connect to services inside kubernetes. - -In both cases, you need to adjust the various integration configuration files and names so that this can work. - -## Buildah (optional) - -[Buildah](https://buildah.io/) is used for local docker image creation during development. See [buildah installation](https://github.com/containers/buildah/blob/master/install.md) - -See `make buildah-docker` for an entry point here. - -## Helm chart development, integration tests in kubernetes - -You need `kubectl`, `helm`, `helmfile`, and a valid kubernetes context. Refer to https://docs.wire.com for details. +file has moved [here](../legacy/developer/dependencies.md) diff --git a/docs/developer/editor-setup.md b/docs/developer/editor-setup.md index 4e030f88b08..b3fd30b7ee1 100644 --- a/docs/developer/editor-setup.md +++ b/docs/developer/editor-setup.md @@ -1,117 +1 @@ -# Editor setup {#DevEditor} - -This page provides tips for setting up editors to work with the Wire codebase. - -## For multiple editors {#DevAll} - -### Using Haskell IDE Engine - -See [official documentation](https://github.com/haskell/haskell-ide-engine) - -In addition, you can generate (and re-generate after changes to stack.yaml) a `hie.yaml` configuration file with - -``` -make hie.yaml -``` - -## Emacs {#DevEmacs} - -### Jump-to-definition {#DevEmacsJump} - -Jump-to-definition is possible with [hasktags][]. First you need to install it and make sure it's on your PATH (if you don't want hasktags to be on your PATH, do `M-x customize-variable haskell-hasktags-path`): - -[hasktags]: https://hackage.haskell.org/package/hasktags - -```bash -stack install hasktags # or cabal install hasktags -``` - -To generate tags, do `M-x haskell-mode-generate-tags`. You can also add a Git hook to regenerate tags on checkout: - -```bash -echo "hasktags -e -x ." > .git/hooks/post-checkout -chmod +x .git/hooks/post-checkout -``` - -To jump to an identifier, press `M-.`. You can also do `C-u M-x xref-find-definitions` to get interactive search through identifiers. Press `M-,` to return to where you were before the jump. - -Jump-to-definition is case-insensitive by default, which is probably not what you want. To change that, do `M-x customize-variable tags-case-fold-search`. - -By default hasktags only generates tags for the current package. The Wire backend is composed of many packages, and it's useful to be able to jump to any identifier in wire-server. One way to do it is to setup Emacs to check if there's a Projectile project that the current directory belongs to, and if so, override the "current package" default. - -Install the [projectile][] package for Emacs and do `M-x projectile-add-known-project `. Then add the following snippet to your `init.el`: - -[projectile]: https://www.projectile.mx/en/latest/installation/ - -``` -(require 'haskell) -(require 'projectile) - -;; When inside a project, even if there is a cabal file in the current -;; folder, use the project folder to generate tags. This is useful for -;; projects with several services or subprojects. -(defadvice haskell-cabal--find-tags-dir (around project-tags act) - (setq ad-return-value - (if (projectile-project-root) - (projectile-project-root) - ad-do-it))) -``` - -### Haskell Language Server - -To use HLS bundled in direnv setup, here is a sample `.dir-locals.el` that can -be put in the root directory of the project: -```el -((haskell-mode . ((haskell-completion-backend . lsp) - (lsp-haskell-server-path . "/home/haskeller/code/wire-server/hack/bin/nix-hls.sh") - ))) -``` - -### Ormolu integration - -There are make targets `format`, `formatf`, `formatc` to re-format -or check the entire repository. This takes about 10 seconds. - -Emacs integration is [linked -here](https://github.com/tweag/ormolu#editor-integration). - -## Vim {#DevVim} - -### hie (Haskell IDE engine) integration - -* Generate `hie.yaml` as described [at the top of this file](#using-haskell-ide-engine) -* You may follow the setup described [in this blog post](http://marco-lopes.com/articles/Vim-and-Haskell-in-2019/) - -### ormolu integration - -There are make targets `format`, `formatf`, `formatc` to re-format -or check the entire repository. This takes about 10 seconds. - -Vim integration is [linked -here](https://github.com/tweag/ormolu#editor-integration). - -If you use sdiehl's module, you you need to collect the language extensions from `package-defaults.yaml`: - -``` - let g:ormolu_options = ["--ghc-opt -XAllowAmbiguousTypes --ghc-opt -XBangPatterns --ghc-opt -XConstraintKinds --ghc-opt -XDataKinds --ghc-opt -XDefaultSignatures --ghc-opt -XDerivingStrategies --ghc-opt -XDeriveFunctor --ghc-opt -XDeriveGeneric --ghc-opt -XDeriveLift --ghc-opt -XDeriveTraversable --ghc-opt -XEmptyCase --ghc-opt -XFlexibleContexts --ghc-opt -XFlexibleInstances --ghc-opt -XFunctionalDependencies --ghc-opt -XGADTs --ghc-opt -XInstanceSigs --ghc-opt -XKindSignatures --ghc-opt -XLambdaCase --ghc-opt -XMultiParamTypeClasses --ghc-opt -XMultiWayIf --ghc-opt -XNamedFieldPuns --ghc-opt -XNoImplicitPrelude --ghc-opt -XOverloadedStrings --ghc-opt -XPackageImports --ghc-opt -XPatternSynonyms --ghc-opt -XPolyKinds --ghc-opt -XQuasiQuotes --ghc-opt -XRankNTypes --ghc-opt -XScopedTypeVariables --ghc-opt -XStandaloneDeriving --ghc-opt -XTemplateHaskell --ghc-opt -XTupleSections --ghc-opt -XTypeApplications --ghc-opt -XTypeFamilies --ghc-opt -XTypeFamilyDependencies --ghc-opt -XTypeOperators --ghc-opt -XUndecidableInstances --ghc-opt -XViewPatterns"] -``` - -If you want to be playful, you can look at how `tools/ormolu.sh` -collects the language extensions automatically and see if you can get -it to work here. - -## VSCode - -The project can be loaded into the [Haskell Language Server](https://github.com/haskell/haskell-language-server). -This gives type checking, code completion, HLint hints, formatting with Ormolu, lookup of definitions and references, etc.. -All needed dependencies (like the `haskell-language-server` and `stack` binaries) are provided by `shell.nix`. - -Setup steps: -- Install the plugins `Haskell` (Haskell Language Server support), `Haskell Syntax` and `Nix Environment Selector` -- Generate the `hie.yaml` file: `make hie.yaml` -- Select the nix environment from `shell.nix` with the command `Nix-Env: Select environment`. -- Reload the window as proposed by the `Nix Environment Selector` plugin - -An alternative way to make these dependencies accessible to VSCode is to start it in the `direnv` environment. -I.e. from a shell that's current working directory is in the project. The drawbacks of this approach are -that it only works locally (not on a remote connection) and one VSCode process needs to be started per project. \ No newline at end of file +file has moved [here](../legacy/developer/editor-setup.md) diff --git a/docs/developer/features.md b/docs/developer/features.md index a07f505d4d3..fc5fed6c1fd 100644 --- a/docs/developer/features.md +++ b/docs/developer/features.md @@ -1,77 +1 @@ -## Features - -Wire has multiple features (or feature flags) that affect the behaviour of -backend and clients, e.g. `sso`, `searchVisibility`, `digitalSignatures`, -`appLock`, `legalhold`, `fileSharing`. - -Features cannot by configured by the users themselves, which distinguishes -features from user properties. Who can configure a feature depends on the -feature itself, e.g: - -- Team administrators via Wire Team Settings -- Customer support via the backoffice tool -- Site administrators by changing a configuration file on the server - -Depending on the feature the configuration can be defined - -- for all users on the server (per-instance) -- per-team -- or per-user - -or a combination of all three levels, where configuration on a per-user level -may override configuration defined for the the team, which may override -configuration defined at the instance level. or instance may override team -(eg. when security settings are enforced so team admins do not have the -power to break security policy). details depend on the individual feature. - -## Feature Configuration API - -The Feature configurations API exposes the feature configuration that results -from combining the settings (per-instance, per-team, per-user) for the user that -queries the endpoint: - -`GET /feature-configs/:feature-name` - -```json -{ - "status": "enabled" /* or "disabled" */ - "config": { - /* ... */ - } -} -``` -where the optional `config` field contains settings that are specific to the feature. - -The configurations for all features can be obtained all at once via `GET -/feature-configs` - -```json -{ - "sso": { - "status": "enabled" - }, - "searchVisibility": { - "status": "disabled" - }, - "appLock": { - "status": "enabled", - "config": { - "enforceAppLock": "true", - "inactivityTimeoutSecs": 300 - } - } - /* ... */ -} -``` - -Note: The implemenation of the Feature Configuration API is re-using the types -from `Wire.API.Team.Feature` (Team Features API). - -## Team Features API - -The Team features API preceedes the notion of the feature configuration API. The -endpoints of the form `GET /teams/:tid/features/:feature-name` and `PUT -/teams/:tid/features/:feature-name` can be used to get and set the feature -configuration on a per-team level. Features that cannot be get and set on a -per-team level may be missing from the Team Features API. See the Swagger -documentation on what endpoints are available. +file has moved [here](../legacy/developer/features.md) diff --git a/docs/developer/federation-api-conventions.md b/docs/developer/federation-api-conventions.md index 84b73a8a6c0..257a6bae26f 100644 --- a/docs/developer/federation-api-conventions.md +++ b/docs/developer/federation-api-conventions.md @@ -1,35 +1 @@ - - -# Federation API Conventions - -- All endpoints must start with `/federation/` -- All path segments must be in kebab-case, and only consist of alphanumeric - characters. The name the field in the record must be the same name in - camelCase. -- There must be exactly one segment after `/federation/`, so - `/federation/foo` is valid, but `/federation/foo/bar` is not. -- All endpoints must be `POST`. -- No query query params or captured path params, all information that needs to - go must go in body. -- All responses must be `200 OK`, domain specific failures (e.g. the - conversation doesn't exist) must be indicated as a Sum type. Unhandled - failures can be 5xx, an endpoint not being implemented will of course - return 404. But we shouldn't pile onto these. This keeps the federator simple. -- Accept only json, respond with only json. Maybe we can think of changing - this in future. But as of now, the federator hardcodes application/json as - the content type of the body. -- Ensure that paths don't collide between brig and galley federation API, this - will be very helpful when we merge brig and galley. -- The name of the path segment after `/federation/` must be either - `-` or `on--`, e.g. - `get-conversations` or `on-conversation-created`. - - How to decide which one to use: - - If the request is supposed to ask for information/change from another - backend which has authority over that information, like send a message or - leave a conversation, then use the first format like `send-message` or - `leave-conversation`. - - If the request is supposed to notify a backend about something the caller of - this request has authority on, like a conversation got created, or a message - is sent, then use the second format like `on-conversation-created` or - `on-message-sent` +file has moved [here](../legacy/developer/federation-api-conventions.md) diff --git a/docs/developer/how-to.md b/docs/developer/how-to.md index f259039cf9e..e9179580fbe 100644 --- a/docs/developer/how-to.md +++ b/docs/developer/how-to.md @@ -1,206 +1 @@ -# Developer how-to's - -The following assume you have a working developer environment with all the dependencies listed in [./dependencies.md](./dependencies.md) available to you. - -## How to look at the swagger docs / UI locally - -Terminal 1: -* Set up backing services: `./deploy/dockerephemeral/run.sh` - -Terminal 2: -* Compile all services: `make services` - * Note that you have to [import the public signing keys for nginx](../../services/nginz/README.md#common-problems-while-compiling) to be able to build nginz -* Run services including nginz: `export INTEGRATION_USE_NGINZ=1; ./services/start-services-only.sh` - -Open your browser at: - -- http://localhost:8080/api/swagger-ui for the swagger 2.0 endpoints (in development as of Feb 2021 - more endpoints will be added here as time goes on) -- http://localhost:8080/swagger-ui/ for the old swagger 1.2 API (old swagger, endpoints will disappear from here (and become available in the previous link) as time progresses) - -Swagger json (for swagger 2.0 endpoints) is available under http://localhost:8080/api/swagger.json - -## How to run federation tests across two backends - -Requirements: - -* `helm` and `kubectl` available on your PATH -* access to a kubernetes cluster via e.g. a `~/.kube/config` file. - -The process consists of: - -1. Inspect/change the multi-backend tests -2. Deploy two backends to kubernetes cluster -3. Run multi-backend test half-locally half-on-kubernetes or fully on kubernetes -4. Teardown - -### 1. Inspect/change the multi-backend test code - -Refer to `services/brig/test/integration/API/Federation/End2End.hs` for the current multi-backend tests. - -*Note that they only run if `INTEGRATION_FEDERATION_TESTS` is set to `1`. This is currently configured to be OFF when running regular brig integration tests (e.g. via `make -C services/brig integration`) but is by default ON when running tests on kubernetes or on CI, or when using the `services/brig/federation-tests.sh` script.* - -### 2. Deploy two backends to kubernetes cluster - -Decide which code you would like to deploy. The following options are detailed in the subsections below. - -* 2.1 Deploy the the latest compiled code from `develop` -* 2.2 Deploy code from your pull request -* 2.3 Deploy your local code to a kind cluster - -#### 2.1 Deploy the the latest compiled code from `develop` - -First, find the latest CI-compiled code made available as docker images: - -``` -# Run all commands from the top directory of wire-server -make latest-tag -``` - -Output might be - -``` -./hack/bin/find-latest-docker-tag.sh -latest tag for brig: -2.104.11 -latest tag for nginz: -2.104.11 -``` - -Let's assume the tags are the same(*) for both, then export an environment variable: - -``` -export DOCKER_TAG=2.104.11 -export NAMESPACE="myname" -make kube-integration-setup -``` - -This will create two full installations of wire-server on the kubernetes cluster you've configured to connect to, and should take ~10 minutes. The namespaces will be `$NAMESPACE` and `$NAMESPACE-fed2`. - - -##### Troubleshooting - -`make latest-tag` gives different tags for brig and nginz: - -* maybe CI hasn't finished, or failed. Look at concourse (`kubernetes-dev` pipeline) - -#### 2.2 Deploy code from your pull request - -*Note: CI already runs multi-backend federation integration tests on your PR, so this section may not be often useful in practice. This is still documented for completeness and to help understand the relation between source code and compiled docker images on CI.* - -Check CI for the latest tag that has been created on your PR (expect this to take at least 30-60 minutes from the last time you pushed to your branch). Example: - -Look at a successful job in the `wire-server-pr` pipeline from a job bruild matching your desired PR and commit hash. Then, find the actual docker tag used. - -![concourse-pr-version-circled](https://user-images.githubusercontent.com/2112744/114410146-69b34000-9bab-11eb-863c-106fb661ca82.png) - -``` -# PR 1438 commit 7a183b2dbcf019df1af3d3b97604edac72eca762 translates to -export DOCKER_TAG=0.0.1-pr.3684 -export NAMESPACE="myname" -make kube-integration-setup -``` - -#### 2.3 Deploy your local code to a kind cluster - -This can be useful to get quicker feedback while working on multi-backend code or configuration (e.g. helm charts) than to wait an hour for CI. This allows you to test code without uploading it to github and waiting an hour for CI. - -FUTUREWORK: this process is in development (update this section after it's confirmed to work): - -##### (i) Build images - -1. Ensure `buildah` is available on your system. -2. Compile the image using `make buildah-docker`. This will try to upload the - images into a `kind` cluster. If you'd prefer uploading images to quay.io, - you can run it with `make buildah-docker BUILDAH_PUSH=1 BUILDAH_KIND_LOAD=0` - -##### (ii) Run tests in kind - -0. Create a local kind cluster with `make kind-cluster` -1. Install wire-server using `make kind-integration-setup`. -2. Run tests using `make kind-integration-test`. -3. Run end2end integration tests: `make kind-integration-e2e`. - -NOTE: debug this process further as some images (e.g. nginz) are missing from the default buildah steps. -* Implement re-tagging development tags as your user tag? - -#### 2.4 Deploy your local code to a kubernetes cluster - -This sections describes how partially update a release with a local build of a service, in this example `brig`. - -Start by deploying a published release (see 2.1 or 2.2). - -``` -export NAMESPACE=$USER -export DOCKER_TAG=2.116.32 -make kube-integration-setup -``` - -Then build and push the `brig` image by running - -``` -export DOCKER_TAG_LOCAL_BUILD=$USER -hack/bin/buildah-compile.sh -DOCKER_TAG=$DOCKER_TAG_LOCAL_BUILD EXECUTABLES=brig BUILDAH_PUSH=1 ./hack/bin/buildah-make-images.sh -``` - -To update the release with brig's local image run -``` -./hack/bin/set-chart-image-version.sh "$DOCKER_TAG_LOCAL_BUILD" brig -./hack/bin/integration-setup-federation.sh -``` - - -## 3 Run multi-backend tests - -### Run all integration tests on kubernetes - -* takes ~10 minutes to run -* test output is delayed until all tests have run. You will have to scroll the output to find the relevant multi-backend test output. -* tests run entirely on kubernetes. -* includes running the federation multi-backend tests by default (see also section (1)) - -``` -make kube-integration-test -``` - -### Run only the multi-backend tests - -* runs faster (~ half a minute) -* test output is shown dynamically as tests run -* business logic code runs on kubernetes, but the test executable runs on your local computer (which connects using `telepresence`) - -1. ensure you have compiled brig-integration: `make -C services/brig fast` -2. ensure you have `telepresence` installed (see developer dependencies documentation) -3. Run the actual tests, (takes half a minute): - -``` -./services/brig/federation-tests.sh "$NAMESPACE" -``` - -Note that this runs your *locally* compiled `brig-integration`, so this allows to easily change test code locally with the following process: - -1. change code under `services/brig/test/integration/Federation/` -2. recompile: `make -C services/brig fast` -3. run `./services/brig/federation-tests.sh test-$USER` again. - -### Run selected integration tests on kuberentes - -To run selective tests from brig-integration: - -``` -helm -n $NAMESPACE get hooks $NAMESPACE-wire-server | yq '.' | jq -r 'select(.metadata.name | contains("brig-integration"))' > /tmp/integration-pod - -# optional: edit the test pattern /tmp/integration-pod - -kubectl apply -n $NAMESPACE -f /tmp/integration-pod -``` - -## 4 Teardown - -To destroy all the resources on the kubernetes cluster that have been created run - -``` -./hack/bin/integration-teardown-federation.sh -``` - -Note: Simply deleting the namespaces is insufficient, because it leaves some resources (of kind ClusterRole, ClusterRoleBinding) that cause problems when redeploying to the same namespace via helm. +file has moved [here](../legacy/developer/how-to.md) diff --git a/docs/developer/linting.md b/docs/developer/linting.md index 5dcbf75ab0f..49d740d3c47 100644 --- a/docs/developer/linting.md +++ b/docs/developer/linting.md @@ -1,55 +1 @@ -# Linting - -# HLint - -To run [HLint](https://github.com/ndmitchell/hlint) you need it's binary, e.g. -by executing: - -```sh -nix-shell -p hlint -``` - -To run it on the whole project (Warning: This takes a long time!): - -```sh -hlint . -``` - -To run it on a sub-project: - -```sh -hlint services/federator -``` - -# Stan - -To run [Stan](https://github.com/kowainik/stan), you need it's binary compiled -by the same GHC version as used in the project. - -```sh -nix-shell -p haskell.packages.ghc884.stan -``` - -Stan depends on [*hie*](https://www.haskell.org/ghc/blog/20190626-HIEFiles.html) -database files that are created during compilation. To generate them for all -packages add this to your `cabal.project.local` file: - -``` -package * - ghc-options: -fwrite-ide-info -hiedir=.hie -``` - -Of course, you can append the `ghc-options` to the respective entry of a package or -add a new one: - -```sh -package cargohold - ghc-options: -fwrite-ide-info -hiedir=.hie -``` - -To analyze a sub-project with stan: - -```sh -cd services/cargohold -stan -``` +file has moved [here](../legacy/developer/linting.md) diff --git a/docs/developer/processes.md b/docs/developer/processes.md index 4f1162c0c14..bda71f73e02 100644 --- a/docs/developer/processes.md +++ b/docs/developer/processes.md @@ -1,50 +1 @@ -# Internal processes - -The following processes only apply to Wire employees working on this code base. - -## Inspect helm charts before releasing them - -`make charts` will create a partial copy of the `./charts` folder under `./.local/charts/` and set helm chart versions as well as docker tags. - -`make chart-` can be used to copy & version-pin a single chart before its release. - -*See the `CHARTS` variable in the top-level Makefile for the current default list of charts.* - -## Run wire-server integration tests inside kubernetes using helm - -You need kubectl, helm, and a configured kubernetes cluster - -```sh -# for "whatever wire-server source code, usually latest develop, I don't care much" -export DOCKER_TAG=$(./hack/bin/find-latest-docker-tag.sh) -echo "$DOCKER_TAG" -# or, if you wish to test specific wire-server source code, e.g. a particular PR already built by CI: -export DOCKER_TAG= - -# The following can take ~15 minutes and will set up an ephemeral kubernetes namespace and run all integration tests. -make kube-integration -``` - -When you're done, you can run - -``` -make kube-integration-teardown -``` - -## Upload Helm charts to our S3 mirror - -Ideally, only CI will do this after commits are merged to develop or master respectively. If this needs to be done manually: - -Ensure you are either on develop or master, and your git working tree is clean. - -```sh -export HELM_SEMVER= # must be a semantic version -export DOCKER_TAG= - -# Upload all charts inside the makefile list: -make upload-charts - -# Upload a single chart, e.g. wire-server -make upload-wire-server -``` - +file has moved [here](../legacy/developer/processes.md) diff --git a/docs/developer/scim/storage.md b/docs/developer/scim/storage.md index bcf64c64154..a070cdb42f5 100644 --- a/docs/developer/scim/storage.md +++ b/docs/developer/scim/storage.md @@ -1,22 +1 @@ -# Storing SCIM-related data {#DevScimStorage} - -_Author: Artyom Kazak, Matthias Fischmann_ - ---- - -## Storing user data {#DevScimStorageUsers} - -SCIM user data is validated by the spar service and stored as brig users. All fields that wire doesn't care about are silently dropped. `GET /scim/v2/Users` will trigger a lookup in brig, and the data thus obtained is synthesized back into a SCIM record. - -Time stamps `created_at` and `last_updated_at` for the SCIM metadata are stored in `spar.scim_user_times`. The are kept in sync with the users that are otherwise stored in brig. (Rationale: we briefly considered using `select writetime(*) from brig.user` for last update and `select writetime(activated) from brig.user` for creation, but this has a drawback: we don't have the time stamps when storing the record, so the `POST` handler would need to do a database write and a consecutive lookup, or an `insert if not exists`.) - -Users created by SCIM set the `ManagedBy` field in brig to `ManagedByScim`. This *should* lead to brig disallowing certain update operations (since the single source of truth should be the SCIM peer that has created and is updating the user), but we never got around to implementing that (as of Wed 15 Jul 2020 10:59:11 AM CEST). See also {@SparBrainDump} (grep for `ManagedBy`). - - -## Storing SCIM tokens {#DevScimStorageTokens} - -[SCIM tokens](../../reference/provisioning/scim-token.md) are stored in two tables in Spar: - -* `team_provisioning_by_token` for `token -> token info` lookups; used to perform authentication. - -* `team_provisioning_by_team` for `team -> [token info]` and `(team, token ID) -> token info` lookups; used to display tokens in team settings, and to decide which tokens should be deleted when the whole team is deleted. +file has moved [here](../legacy/developer/scim/storage.md) diff --git a/docs/developer/servant.md b/docs/developer/servant.md index 0b577f7f2cf..de2b8599d52 100644 --- a/docs/developer/servant.md +++ b/docs/developer/servant.md @@ -1,58 +1 @@ -# Introduction - -We currently use Servant for the public (i.e. client-facing) API in brig, galley and spar, as well as for their federation (i.e. server-to-server) and internal API. - -Client-facing APIs are defined in `Wire.API.Routes.Public.{Brig,Galley}`. Internal APIs are all over the place at the moment. Federation APIs are in `Wire.API.Federation.API.{Brig,Galley}`. - -Our APIs are able to generate Swagger documentation semi-automatically using `servant-swagger2`. The `schema-profunctor` library (see [`README.md`](/libs/schema-profunctor/README.md) in `libs/schema-profunctor`) is used to create "schemas" for the input and output types used in the Servant APIs. A schema contains all the information needed to serialise/deserialise JSON values, as well as the documentation and metadata needed to generate Swagger. - -# Combinators - -We have employed a few custom combinators to try to keep HTTP concerns and vocabulary out of the API handlers that actually implement the functionality of the API. - -## `ZAuth` - -This is a family of combinators to handle the headers that nginx adds to requests. We currently have: - - - `ZUser`: extracts the `UserId` in the `Z-User` header. - - `ZLocalUser`: same as `ZUser`, but as a `Local` object (i.e. qualified by the local domain); this is useful when writing federation-aware handlers. - - `ZConn`: extracts the `ConnId` in the `Z-Connection` header. - - `ZConversation`: extracts the `ConvId` in the `Z-Conversation` header. - -## `MultiVerb` - -This is an alternative to `UVerb`, designed to prevent any HTTP-specific information from leaking into the type of the handler. Use this for endpoints that can return multiple responses. - -## `CanThrow` - -This can be used to add an error response to the Swagger documentation. In services that use polysemy for error handling (currently only Galley), it also adds a corresponding error effect to the type of the handler. The argument of `CanThrow` can be of a custom kind, usually a service-specific error kind (such as `GalleyError`, `BrigError`, etc...), but kind `*` can also be used. - -Note that error types can also be turned into `MultiVerb` responses using the `ErrorResponse` combinator. This is useful for handlers that can return errors as part of their return type, instead of simply throwing them as IO exceptions or using polysemy. If an error is part of `MultiVerb`, there is no need to also report it with `CanThrow`. - -## `QualifiedCapture` - -This is a capture combinator for a path that looks like `/:domain/:value`, where `value` is of some arbitrary type `a`. The value is returned as a value of type `Qualified a`, which can then be used in federation-aware endpoints. - -# Error handling - -Several layers of error handling are involved when serving a request. A handler in service code (e.g. Brig or Galley) can: - - 1. return a value on the `Right`; - 2. return a value on the `Left`; - 3. throw an `IO` exception. - -The `Handler → Servant.Handler` function, together with Servant's internal response creation logic, will, respectively: - - 1. produce a normal response; - 2. produce an error response, possibly logging the error; - 3. (ignore any `IO` exceptions, and let them bubble up). - -Finally, the error-catching middleware `catchErrors` in `Network.Wai.Utilities.Server` will: - - 1. let normal responses through; - 2. depending on the status code: - - if < 500, let the error through; - - if >= 500, wrap the error response in a JSON error object (if it is not already - one), and log it at error level. - 3. catch the exception, turn it into a JSON error object, and log it. The - log level depends on the status code (error for 5xx, debug otherwise). +file has moved [here](../legacy/developer/servant.md) diff --git a/docs/developer/testing.md b/docs/developer/testing.md index 0e41f8920bf..ae5e8568bda 100644 --- a/docs/developer/testing.md +++ b/docs/developer/testing.md @@ -1,25 +1 @@ -## Testing the wire-server Haskell code base - -Every package in this repository should have one or more directories -named `test` or `test*`. Ideally, there are at least unit tests -(eg. in `test/unit`) for libraries, and and at least unit and -integration tests (eg. `test/integration`) for packages with -executables (`/services`). - -### General rule - -Write as much pure code as possible. If you write a function that has -an effect only in a small part of its implementation, write a pure -function instead, and call if from an effectful function. - -### Unit tests - -All data types that are serialized ([`ToByteString`](https://hackage.haskell.org/package/amazonka-core-1.6.1/docs/Network-AWS-Data-ByteString.html#t:ToByteString), [`ToByteString`](https://hackage.haskell.org/package/aeson/docs/Data-Aeson.html#t:ToJSON), swagger, cassandra, ...) should have roundtrip quickcheck tests like [here](https://github.com/wireapp/wire-server/blob/develop/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs#L235). -All pure functions `f` that do something interesting should have a couple of tests of the form `shouldBe (f ) ` to cover corner cases. -If code is refactored, all the effects should be mocked with polysemy or mtl, the old implementation should be moved to the test suite, and there should be quickcheck tests running the new implementation against the old and comparing results ([example](https://github.com/wireapp/wire-server/blob/develop/services/gundeck/test/unit/MockGundeck.hs)). - -### Integration tests - -- All new rest API end-points need to be called a few times to cover as much of the corner cases as feasible. -- We have machinery to set up scaffolding teams and modify context such as server configuration files where needed. Look through the existing code for inspiration. -- Test only semantics, not syntax: do not post verbatim json to an end-point, but use the Haskell data types to render the json that you post. +file has moved [here](../legacy/developer/testing.md) diff --git a/docs/README.original.md b/docs/legacy/README.md similarity index 100% rename from docs/README.original.md rename to docs/legacy/README.md diff --git a/docs/legacy/developer/api-versioning.md b/docs/legacy/developer/api-versioning.md new file mode 100644 index 00000000000..d5ee1892666 --- /dev/null +++ b/docs/legacy/developer/api-versioning.md @@ -0,0 +1,464 @@ +# Status of this document + +This is a proposal for API versioning that we may adopt in the future. +If you don't find any trace of it in the code base, that means you're +not to late to submit a PR yourself! :) + +# Introduction + +Since upgrades cannot happen instantaneously, we need to release wire +backends and wire client apps that work together accross releases. + +In the past, we have made sure that the api is changed in a way that +the backend can be newer than the client, and then releasing the +change on the backend first. This worked well on the cloud where we +had control, but fails a lot in the on-prem setting: if we add a new +end-point to the API, the backend will still be able to handle older +clients that simply don't know about that end-point, but the client +won't handle old backends well, since it will try to call the +end-point, and fail. + +The problem becomes more complicated still if you think about backends +talking to other backends in the context of federation: An HTTP server +will act as a client inside the handler function, and the version it +responds with may differ from the version it talks to another backend +it needs to query. + +The new approach outlined here introduces API versions to address this +problem. Every API version is only compatible with itself, but every +node in the network can support a *set of API versions*. A HTTP +client can query the set of supported versions from an HTTP server, +and then pick one that works for it. + +We believe this is a good approach to solve all our API compatibility +problems both between apps and backends and between backends in the +context of federation, but we also list some open questions (and +probably forget to list more). + +In the following, we will refer to HTTP clients as "clients", no +matter whether it is an app or a backend talking to another backend +(federation); and to HTTP servers as "server", no matter which API is +it serving (federation or app). + + +# Versions and servant routing tables + +All routing tables for which a new version is born will be changed +into taking the version number as a parameter, which is added as a +prefix to every route: + +```haskell +data Api (version :: Symbol) routes = Api + { getUnqualifiedConversation :: + routes + :- version + :> Summary "Get a conversation by ID" + :> ZLocalUser + :> "conversations" + :> Capture "cnv" ConvId + :> Get '[Servant.JSON] Conversation, + getConversation :: + routes + :- version + :> Summary "Get a conversation by ID" + :> ZLocalUser + :> "conversations" + :> QualifiedCapture "cnv" ConvId + :> Get '[Servant.JSON] Conversation, + [...] + } +``` + +APIs of all the supported versions can be composed like this: + +```haskell +type ServantAPI = + ToServantApi (Api "v1") + :<|> ToServantApi (Api "v2") + :<|> ToServantApi (Api "v4") -- v3 is broken +``` + +This will result in a routing table like this one: + +``` +/v1/users/... +/v1/conversations/... +/v2/users/... +/v2/conversations/... +/v4/users/... +/v4/conversations/... +``` + + +## Changes between versions + +The point of having versions is of course not that all of them look +exactly the same except for their prefix. The point is that there are +other things that change between versions. + +There are essentially two categories of changes: + +1. **data**: request or response bodies, variable path segments, + possible headers or status codes (as in `UVerb` or `MultiVerb`), + etc. +2. **structure**: literal path segments, verb, the version itself, ... + + +## Changes in the data + +If a data type in request, response, variable path segments, or anywhere else +changes, introduce a type family parameterized in the version. + +```haskell +[...] + getConversation :: + routes + :- version + :> Summary "Get a conversation by ID" + :> ZLocalUser + :> "conversations" + :> QualifiedCapture "cnv" ConvId + :> Get '[Servant.JSON] (ConversationV version), +[...] + +type family ConversationV (version :: Symbol) :: * where + ConversationV "v1" = Conversation + ConversationV "v2" = Conversation + ConversationV "v4" = ConversationV4 +``` + +Note that before version `"v4"`, nothing changed for this type, so +there was no need to introduce a new concrete data type. + +If the last change of a data type is entirely phased out, the type +family turns constant and can be removed again. If you see this in +your code: + +```haskell +type family ConversationV (version :: Symbol) :: * where + ConversationV "v4" = ConversationV4 + ConversationV "v5" = ConversationV4 + ConversationV "v6" = ConversationV4 + ConversationV "v7" = ConversationV4 +``` + +You can remove 'ConversationV', rename `ConversationV4` to Conversation, +and use it in the routing table instead of `ConversationV` again, as +before `"v4"`. + +### Open questions + +Adding/removing a new version now requires to touch a lot of type +families. The changes are trivial and the compiler will lead us to +all of them, but there are potentially many. + +It would be nice to have a default type that is used for all versions +that don't explicitly mention a given type, then future versions would +only have to be touched if a type actually changes. However, (a) it's +not clear to us how to accomplish that (open type families plus +multi-param type classes plus overlapping instances? type-level +'Maybe' with type-level default type?); and (b) it is less robust and +transparent, and depending on the solution carries the risk of missing +a spot where we want to update a type, but the compiler picks the +wrong default. + + +## Changing structure + +Without loss of generality, we only consider additions and deletions +of routes in this section: if you want to change the path or verb of +an end-point, add a new path instead, and phase the old one out (now +or in some future version). + +When end-points are present in some supported versions, but not in +others, their record fields in the servant routing type needs to be +present for all versions, but in some versions should behave as if it +weren't. + +This is best solved by a new data type: + +```haskell +data NotInThisVersion = NotInThisVersion + -- (The value constructor may be needed to implement the handler) + -- (Or something involving `Verb 'NOTINTHISVERSION`?) +``` + +The entire route will then be a type family over the version that maps +all unsupported versions to `NotInThisVersion`. + +Now we can write a type family that can crawl a `ServantAPI` (not the +record one, the one with `:<|>`) and drop all the routes marked as not +existing. + +This will save us the trouble of writing lots of instances for +`NotInThisVersion` (server, swagger, client, ...), and yield exactly +the desired result: + +```haskell +type ServantAPI = + DropNotInThisVersion + ( ToServantApi (Api "v1") + :<|> ToServantApi (Api "v2") + :<|> ToServantApi (Api "v4") -- v3 is broken + ) +``` + + +## Adoption of versioned APIs + +When API versions are introduced to a code base that has a routing +table without versions, the question arises what to do with old +clients or servers talking to new ones. + +We define a middleware that + + (1) maps requests without version prefix in the path to ones that + have version `"v0"`. + + (2) responds with a specific type of error if an unsupported version + is requested (so the client can re-negotiate a new version to + speak after an upgrade, see below). + + +## Version handshake + +Client and server need to agree on a version to use. The server +provides two (kinds of) end-points for that. + +``` +GET /api-versions +=> { "supported": [1, 2, 45, 119] } + +GET /v*/api-docs +=> +``` + +The client developer can pull the swagger docs of a new version and +diff it against the one they already support, and work their way +through the changes (see below). + +The client will call `GET /api-versions` and pick any number in the +intersection of the versions it supports and the ones the server +supports (usually the largest). + +*Corner case:* if we want to distinguish between backend-to-backend +and client-to-backend, we can do that in path suffixes (`GET +/api-versions/{client,federation}` etc.). + + +### No shared api version + +If the intersection set is empty, the client has no way of talking to +the server. It needs to politely fail with an error message about +upgrading, downgrading, or talking to another server. + +This should only happen if the distance between last upgrade on client +and server exceeds the agreed-upon limits (eg., 6 months). + + +### Update detection and version re-negotation + +If the server is upgraded and some old supported versions are phased +out, the client may be caught by surprise. + +Servers should respond with status `404`, error label +`unsupported-version`. The versioning middleware can do that (see +above). + +Client should install a catch-all that will handle this specific +error, re-fetch `/api-versions`, and try again. + +This will only happen if backend and client grow apart in time beyond +the supported limit, and there is some chance it will result in an +empty set of compatible versions, so it's also ok to just fail here. + + +## Strongly typed versions + +If we make version an ADT `ApiVersion`, we can remove old versions +from it in one place and have the compiler guide us through all the +places where we need to remove it. + +There are at least two ways to implement this: + +1. Add a few extra servant instances for `(v :: ApiVersion) :> route`. +2. Define a type family `Versions` that maps `V*` to `"v*"`, and write +`Versions version :>` in the routing type instead of `"v1"`. + +2 seems a lot less work to write, read, and understand. + + +## Data migration (aka data marshalling) + +If the shape of an end-point changes between versions (if a data type +in the routing table becomes a type family), it is often possible to +write marshalling functions that translate a value from an older +version into one of a newer version or vice versa. + +These functions are called marshalling functions and are useful to +define separately to keep the application logic clean. + +For certain changes to a data type used in an API, marshalling is +straight-forward in both directions. The most common example is +adding an optional attribute to a JSON object: + +- *backward migration*: remove the new attribute. +- *forward migration*: set the new attribute to `null`. + +(This is what wire has traditionally done to accomplish client +backwards compatibility without any API versioning.) + +If a mandatory attribute is added in a newer version, there may be a +plausible default value that can be used in the forward migration +(backward migration would still remove the field). + +In other cases, whether there is an automatic migration depends on the +use case and the semantics. + +It may even be impossible to marshal either in one or in both +directions. In this case, you have 3 options: + +1. abandon compatibility; +2. rethink your new version and craft it in a way that two-way + marshalling is possible; +3. make the application work around the gap, eg. by gracefully + refusing to offer video conferencing in a client if it is not + supported on the server yet. + + +## Writing client code + +If you write all code by hand and don't generate anything from the +swagger docs, just look at the swagger diff for every new version and +take it from there. + +If you generate, say, typescript or kotlin or swift from swagger: + +0. have a generated source module `Gen.ts`, plus a source module with + manually written code `Man.ts`. Re-export everything from `Gen.ts` + in `Man.ts`, and only import `Man.ts` in any modules that contain + application logic. + +1. look at the diff of the swagger of the last supported and the new + versions. + +2. copy all functions for routes that have changed from `Gen.ts` to + `Man.ts`. these are speaking an old api version and won't need to + be re-generated any more. work the last version supported by this + function into the name somehow (eg., suffix `"_v23"`). + +3. for every function that moved to `Man.ts` in this way, write a + function *without* the version suffix. It somehow knows the api + version of the server that it talks to (function parameter, app + config, dosn't matter), and decides based on that whether to call + the deprecated function with the `"_v23"` suffix or the one from + `Gen.ts`. If the old one is called, it may have to do some + marshalling of request and response (see above). + +It will happen that a new client will not be able to accomplish +something with an old API. (Example: if video calling is introduced in +`"v12"`, you can't emulate `POST /video-call` when talking to a `"v9"` +server. In these cases, the function in `Man.ts` must raise a "server +too old" exception, and gracefully shut down the new functionality.) + + +### Open questions + +It will be interesting to develop a good flow for doing this given the +concrete tooling that's in place: Should every API version get its own +library, or just a source module? Where is the diff to be applied +exactly? On the swagger? Or on the generated code? How do we make +sure the old versions replaced with new versions that have not changed +aren't in the way? + +For the user apps, the task here is to write (or construct) query +function that picks a version and either succeeds or fails. In the +context of federation, the task is to write handlers for all version +(or a handler that can handle all versions). Inside this handler, +when calling other backends as a client, it is not guaranteed that +those backends can be called in the version the handler is operating +under. + +In the simple cases, this can (probably?) be solved by writing +version-oblivious query functions that intially are just aliases for +the generated client functions, but are manually adjusted to being +able to call other servers on all the supported versions. But it will +be interesting to see where the hard cases happen, and if we'll see +them when they do. + + +## Concerns and design alternatives + +### Why not version every end-point separately? + +Yes, that would work in principle. On the backend, it would make the +entire routing table smaller (no need to concatenate the same +end-point for many versions), which may result in shorter compile +times. On the clients, with a new API version it would be +straight-forward to see which end-points need to be worked on, and +which remain unchanged. + +On the other hand, the routing table size may not be an issue, and if +it is there are solutions (introduce a CCP switch to compile only the +most recent API version that you're working on); and the client +process is already quite straight-forward with the approach outlined +above via diffing the swagger docs between most recent version and +predecessor. + +Plus, if the entire API has one version, you get a few advantages: + +1. The fact that clients are forced to commit to a concrete API + version for all end-points when talking to the backend reduces + testing complexity. If there is a mapping of end-points to + versions, the behavior of interacting parties is much less + restricted, and versions that have not been tested against each + other may be used together. (This can be avoided, but it's less + obvious how to get it right, and testing complexity will likely be + worse.) + +2. The "one version" approach makes it obvious which end-points are in + the most recent API at any given point in time. The "one version + per end-point" approach would either yeild a noisy union of all + supported versions, or there would have to be a mechanism for + reconstructing something close to what we get for free otherwise. + +3. The backend code is a good combination of concise and type-safe in + the "one version" approach. If every end-point had its own + version, the routing table entry would either have to accept a + variable path segment for the version, and fail at run-time if the + version is not supported, or you would have to add one handler per + supported version (even if in the case where all versions call the + same handler function with slightly different parameters). + + +### Syntactical vs. behavioral changes + +It is quite common that behavior of end-points changes together with +the syntax, or even without a change in the syntax. + +This is not a fundamental problem: since the handler can be called +with the version as a type parameter, there is no reason why it +shouldn't change behavior with or without changing the syntax. In +each such case, it needs to be decided whether the difference is +significant enough to justify a new API version. + +At the very least though it should result in diverging swagger docs +that explains those differences. + + +### Client capabilities + +Wire supports client capabilities to decide whether a client should be +allowed to use certain parts of the API. + +This is another alternative to API versions, and it is in some ways +more straight-forward to decide who to interpret capability sets. But +this approach has its own problems: Most importantly, the number of +supported capability sets grows quadratically (not in practice, +because historically clients will only ever support a small part of +all possible combinations of capabilities, but that makes thigns +worse: it makes the system more complex, and then doesn't use that +complexity for anything). + +Therefore, the capabilities we're using in the wire code base should +be gracefully phased out and replaced by API versions. diff --git a/docs/legacy/developer/architecture/wire-arch-2.png b/docs/legacy/developer/architecture/wire-arch-2.png new file mode 100644 index 00000000000..c991a43f631 Binary files /dev/null and b/docs/legacy/developer/architecture/wire-arch-2.png differ diff --git a/docs/legacy/developer/architecture/wire-arch-2.xml b/docs/legacy/developer/architecture/wire-arch-2.xml new file mode 100644 index 00000000000..47b155426de --- /dev/null +++ b/docs/legacy/developer/architecture/wire-arch-2.xml @@ -0,0 +1 @@ +7Vxbc5s4FP41fqwHENfHxkm7nWky6XpnNnnqyKBgWow8ICf2/vqVQGCEZAfHYFLHyUOQEAK+79x0dMgITBbrrylczm9xgOKRoQXrEbgeGYZuWRr9w3o2RY+hGbwnTKOAj9p2TKP/EO8sh62iAGXCQIJxTKKl2OnjJEE+EfpgmuIXcdgTjsW7LmGIpI6pD2O5998oIPOi1zWcbf9fKArn5Z112yvOzKD/O0zxKuH3GxngKf8pTi9gORd/0WwOA/xS6wI3IzBJMSbF0WI9QTEDt4StuO7LjrPVc6coIW0uMIsLnmG8QuUT589FNiUWL/OIoOkS+qz9Qgkfgas5WcS0pdPDjKT4d4US63mK4niCY5zm15fvT/txQjjVuknbAczmKBCmqV2m5T/0jPxK/C2fUUrQutbFX/ErwgtE0g0dwuXPtPklL1sydbOU0nmdSZt3Qi5BYTXZFkV6wIFUgwo+BqhaS0xNvQtMvT5AbYBXBznBCXoVT/Oa/R6JJz9rmvyNOL66Z0kAA2Cr8DWPx9eS0EQBNYm8iVMyxyFOYHyz7b3K7VwOjiYijdYReagdP7IhY4u1EvpcD2LzkU+QEZiSz8x6b+HP+75E7LHzMSgJyhF+DLMs8otOPoTd8BciZMNZhSuCadf28b9jvNxB/k4KM7xK/dIxlN4IpiHiwwDHjgG2l+gUxZBEz6KPOYY0+yjS9LakDUzMbiu2T5ryxvVaaG14681U//r5w4ofbGNlZcDV73483DnPn0x7KP6dPpRW+xBKqxtDkeb2obQfhDRzKNK8o0nz8YJBuFPrCpZKChkh5dJAYmzLR500XSSN07plTDuAsd0m9yguu+ONX3qPI3rbKoSyNU+8pBAePqrBdnXbVgJQxrCnkgC9ocI0OFQ5uHuURvQtUPqq0PSv5n+q0JQ3asbdPQhRuSg420WMZZkCmGVepL5I1BVrRK8DG1269PNFl86mFlVhDa7L8HayRNRBL+GmZPbea3TStyGyNJFdYDe8WWEdJUN0+EQdWrReEojvSefKjFalcvYpVc6S4L1ZU29PJViCmb4PUSHbQE4BJoyjMGGaRFFikcQVQyfyYfyZn1hEQZBrs4rKGluGlms8oVqA2XWfvI5yjRaQE7imwo0YHWQadVvC/FvyATE3gHM6zB0J8wm16jAJUjgy7JiBPEvpUciO/kZBlElcCKFvZR+0A+mh1sPS2C/tj+EMxfc4izi0ElffGwNmmBC8UJBJmLdhwfmSPexiHbJ9szF+eop8NA4ggTOYoaw6OsQrtbdkricmmAGQgzPgKiwZ6IBgVyJ4meL15pxZ/OmvZioq8zZ/Y8WG3cG8eo5AazVDXW8dmVbL6IBWeVfGp7EFnrMN1wu1xwf8rrg2rfToBNwa/WU8yrzGJ22sGVVPI3fZPrXRKh/2zrIbVVlDLb1Zxs4dLzQoAnBTG7BkcX8micJhgb8hpzJmaRRelL6LFYc7nNLLOZQQxjG6OOoOiLXtAYmVi1JCZqr93xdmOwit3QazJ4zBFDVce9ZON9QNUkSnCKb+/Jyp72cNZZVJ0AHWUIacDKoRfWHyQGPsDMiknGJ6xKt/mK2SFHa6xCR62ihOsFB7EuNVIJ/7inFIJcHQbuHyjDIlPnvbbLxMo2dIqHHPX74X6fBE4TBdudjRUiR9QQe5MEPOhU3p09DrtJztLynmafsLp4ctph1nOFLl/Nf0ZlqwOv3BD643CVzg6ytZn+/QeoHl7n9eojjCF1l4yxrLGk4W5KTZ9K4uCxc6D4+/7Qad8i5dX3SWc9ToTMIoWXfIYxPfReZDNM5Jo+BmaBxSfF/gRqa82gltgv52EXh156qQiS6U1BX3sctmnVTFhpTVBalyesuHCaXjfLRzuNWyrjnDJUJAL1UsYtG8IRbgjoQEt9G2pL7X8tryowXhQ4bBymvB2ZeR6BpoFpKoarcMWehN3eoAYDl3cGYAu+ag+H70opHyrKmJNFTe+ARVJEBeOX+saqldJJyyfArIK13Z4UqfKdR4EEwGa9xDwkgsgg/NHElbwW03Zvd+LrazHLWdO/UU7tRr6U5bxrPHlpe64pIXeA2225aX2qAxkdmYqLvyUlMhO70Ea8PHY22F5Ugp8ICYBAPNT2baSkHT3xqe25sUtIgOlZUeqpqTRrlJzv5Oht70xeBp1Ln5ubiEf2t1Fnm0+9PmvV/PvuvIVPK9CmnZ7Y4NKfRX1ZD3FZqqUT/uK9Rz+N7/9Q/EG5r+qoUoVrGdm2zbNseix9XLGvVDdd3T9eZUXmOqHeq+u4hMfSdd13TlM2+ltpiyW2PS4r+EtPcUY02zRW8BNOcVyVGWLpb6weodC9mrlAQA3tzOxxqbWqM5W3vhrAcYe42vHLYO5dYkYylFl3+IX7PkbYFH+uS0Z/r59jv98y24z/d8mpt6Gz5q8u2WsYpQKo9R9TTE/I/NSx+0a3SkY3as8XD7gGqxkev0MvpG50PvUdsOx9LtNOg+ZZ2emu69xXvny3lfCu2Ki6Iei7poc/tv+gqnsf1niODmfw== \ No newline at end of file diff --git a/docs/legacy/developer/cassandra-interaction.md b/docs/legacy/developer/cassandra-interaction.md new file mode 100644 index 00000000000..8cc9d3ee514 --- /dev/null +++ b/docs/legacy/developer/cassandra-interaction.md @@ -0,0 +1,98 @@ +# Writing code interacting with cassandra + + + +* [Anti-patterns](#anti-patterns) + * [Anti-pattern: Using full table scans in production code](#anti-pattern-using-full-table-scans-in-production-code) + * [Anti-pattern: Using IN queries on a field in the partition key](#anti-pattern-using-in-queries-on-a-field-in-the-partition-key) + * [Anti-pattern: Designing for a lot of deletes or updates](#anti-pattern-designing-for-a-lot-of-deletes-or-updates) +* [Understanding more about cassandra](#understanding-more-about-cassandra) + * [primary partition clustering keys](#primary-partition-clustering-keys) + * [optimizing parallel request performance](#optimizing-parallel-request-performance) +* [Cassandra schema migrations](#cassandra-schema-migrations) + * [Backwards compatible schema changes](#backwards-compatible-schema-changes) + * [Backwards incompatible schema changes](#backwards-incompatible-schema-changes) + * [What to do about backwards incompatible schema changes](#what-to-do-about-backwards-incompatible-schema-changes) + + + +## Anti-patterns + +### Anti-pattern: Using full table scans in production code + +Queries such as `select some_field from some_table;` are full table scans. Cassandra is not optimized at all for such queries, and even with a small amount of data, a single such query can completely mess up your whole cluster performance. We had an example of that which made our staging environment unusable. Luckily, it was caught in time and [fixed](https://github.com/wireapp/wire-server/pull/1574/files) before making its way to production. + +Suggested alternative: Design your tables in a way to make use of a primary key, and always make use of a `WHERE` clause: `SELECT some_field FROM some_table WHERE some_key = ?`. + +In some rare circumstances you might not easily think of a good primary key. In this case, you could for instance use a single default value that is hardcoded: `SELECT some_field FROM some_table WHERE some_key = 1`. We use this strategy in the `meta` table which stores the cassandra version migration information, and we use it for a [default idp](https://github.com/wireapp/wire-server/blob/4814afd88b8c832c4bd8c24674886c5d295aff78/services/spar/schema/src/V7.hs). `some_field` might be of type `set`, which allows you to have some guarantees. See the implementation of unique claims and the [note on guarantees of CQL +sets](https://github.com/wireapp/wire-server/blob/develop/services/brig/src/Brig/Unique.hs#L110) for more information on sets. + +### Anti-pattern: Using IN queries on a field in the partition key + +Larger IN queries lead to performance problems. See https://lostechies.com/ryansvihla/2014/09/22/cassandra-query-patterns-not-using-the-in-query-for-multiple-partitions/ + +A preferred way to do this lookup here is to use queries operating on single keys, and make concurrent requests. One way to do this is with the [`pooledMapConcurrentlyN`] (https://hoogle.zinfra.io/file/root/.stack/snapshots/x86_64-linux/e2cc9ab01ac828ffb6fe45a45d38d7ca6e672fb9fe95528498b990da673c5071/8.8.4/doc/unliftio-0.2.13/UnliftIO-Async.html#v:pooledMapConcurrentlyN) function. To be conservative, you can use N=8 or N=32, we've done this in other places and not seen problematic performance +yet. For an optimization of N, see the section further below. + +### Anti-pattern: Designing for a lot of deletes or updates + +Cassandra works best for write-once read-many scenarios. + +Read e.g. +- https://www.datastax.com/blog/cassandra-anti-patterns-queues-and-queue-datasets +- https://www.instaclustr.com/support/documentation/cassandra/using-cassandra/managing-tombstones-in-cassandra/# +- search the internet some more for 'cassandra' and 'tombstones' and if you find good posts, add them here. + +## Understanding more about cassandra + +### primary partition clustering keys + +Confused about primary key, partition key, and clustering key? See e.g. [this post](https://blog.devgenius.io/cassandra-primary-vs-partitioning-vs-clustering-keys-3b3fa0e317f4) or [this one](https://dzone.com/articles/cassandra-data-modeling-primary-clustering-partiti) + +### optimizing parallel request performance + +See the thoughts in https://github.com/wireapp/wire-server/pull/1345#discussion_r567829234 - measuring overall and per-request performance and trying out different settings here might be worthwhile if increasing read or write performance is critical. + +## Cassandra schema migrations + +### Backwards compatible schema changes + +Most cassandra schema changes are backwards compatible, or *should* be designed to be so. Looking at the changes under `services/{brig,spar,galley,gundeck}/schema` you'll find this to be mostly the case. + +The general deployment setup for services interacting with cassandra have the following assumption: + +* cassandra schema updates happen *before* new code is deployed. + * This is safeguarded by the concourse deployment pipelines + * This is also safeguarded by the `wire-server` helm chart, which deploys the `cassandra-migrations` job as part of a helm [pre-install/pre-upgrade hook](https://github.com/wireapp/wire-server/blob/b3b1af6757194aa1dc86a8f387887936f2afd2fb/charts/cassandra-migrations/templates/migrate-schema.yaml#L10-L13): that means the schema changes are applied, and helm waits before launching the new code in the brig/galley/spar/gundeck pods until the changes have completed applying. + * This is further safeguarded by the code e.g. in brig refusing to even start the service up if the applied schema migration is not at least at a version the code expects it to. See [versionCheck](https://github.com/wireapp/wire-server/blob/b3b1af6757194aa1dc86a8f387887936f2afd2fb/services/brig/src/Brig/App.hs#L411) and [schemaVersion](https://github.com/wireapp/wire-server/blob/b3b1af6757194aa1dc86a8f387887936f2afd2fb/services/brig/src/Brig/App.hs#L136-L137) + +So usually with these safeguards in place, and backwards-compatible changes, we have the following: + +* At time t=0, old schema, old code serves traffic; all good. +* At time t=1, new schema, old code serves traffic: all good since backwards compatible. +* At time t=2, new schema, old code AND new code serve traffic: all good since backwards compatible. +* At time t=3, new schema, new code serves traffic: all good! + +If this order (apply schema first; then deploy code) is not safeguarded, then there will be code running in e.g. production which `SELECT my_new_field FROM my_new_table` even though this doesn't yet exist, leading to 500 server errors for as long as the mismatch between applied schema and code version persists. + +### Backwards incompatible schema changes + +In the case where a schema migration is **not backwards compatible**, such as in the form of `ALTER TABLE my_table DROP my_column`, the reverse problem exists: + +During a deployment: + +* At time t=0, old schema, old code serves traffic; all good. +* At time t=1, new schema, old code serves traffic: 500 server errors as the old code is still active, bit columns or tables have been deleted, so old queries of `SELECT x from my_table` cause exceptions / HTTP 5xx results. + * In the worst deployment scenario, this could raise an alarm and lead operators or automation to stop the deployment or roll back; but that doesn't solve the issue, 500s will continue being thrown until the new code is deployed. +* At time t=2, new schema, old code AND new code serve traffic: partial 500s (all traffic still serves by old code) +* At time t=3, new schema, new code serves traffic: all good again. + +#### What to do about backwards incompatible schema changes + +Options from most to least desirable: + +* Never make backwards-incompatbile changes to the database schema :) +* Do changes in a two-step process: + * First make a change that stops the code from using queries involving `my_column` or `my_table` (assuming you wish to remove those), and deploy this all the way across all of your environments (staging, prod, customers, ...). + * After all deployments have gone through (this can take weeks or months), do the schema change removing that column or table from the database. Often there is no urgency with removal of data, so it's fine to do this e.g. 6 months later. +* Do changes in a one-step process, but accept partial service interruption for several minutes up to a few hours, accept communication overhead across teams to warn them about upcoming interruption or explain previous interruption; accept communication and documentation to warn all operators deploying that or a future version of the code, and accept some amount of frustration that may arise from a lack of said communication and understanding of what is happening. diff --git a/docs/legacy/developer/changelog.md b/docs/legacy/developer/changelog.md new file mode 100644 index 00000000000..886df37964a --- /dev/null +++ b/docs/legacy/developer/changelog.md @@ -0,0 +1,41 @@ +The wire-server repo has a process for changelog editing that prevents +merge conflicts and enforces a consistent structure to the release +notes. + +*Introduced in https://github.com/wireapp/wire-server/pull/1749.* + +## tl;dr + +Entries have to be written in individual files in a relevant subfolder of `./changelog.d/`. + +*Example*: create the file `./changelog.d/2-features/potato-peeler` with one-line contents `Introduce automatic potato peeler functionality when buying potatoes, see [docs](link-to-docs)` + +## Details + +On every pull request, one is supposed to create a new file in the +appropriate subdirectory of `changelog.d`, containing just the text of +the corresponding changelog entry. There is no need to explicitly +write a PR number, because the `mk-changelog.sh` script will add it +automatically at the end. The name of the file does not matter, but +please try to make it unique to avoid unnecessary conflicts (e.g. use +the branch name). + +It is still possible to write the PR number manually if so desired, +which is useful in case the entry should refer to multiple PRs. In +that case, the script leaves the PR number reference intact, as long +as it is at the very end of the entry (no period allowed afterwards!), +and in brackets. It is also possible to use the pattern `##` to refer +to the current PR number. This will be replaced throughout. + +Multiline entries are supported, and should be handled +correctly. Again, the PR reference should either be omitted or put at +the very end. If multiple entries for a single PR are desired, one +should create a different file for each of them. + +## Generating a CHANGELOG for a release + +Just run the script `changelog.d/mk-changelog.sh` with no +arguments. It will print all the entries, nicely formatted, on +standard output. The script gets PR numbers from the `git` log. If +that fails for any reason (e.g. if an entry was added outside of a +PR), make sure that the entry has a manually specified PR number. diff --git a/docs/legacy/developer/dependencies.md b/docs/legacy/developer/dependencies.md new file mode 100644 index 00000000000..0d5d01619f3 --- /dev/null +++ b/docs/legacy/developer/dependencies.md @@ -0,0 +1,248 @@ +# Dependencies {#DevDeps} + +This page documents how to install necessary dependencies to work with the wire-server code base. + +This repository makes use of git submodules. When cloning or updating, use `git submodule update --init --recursive` to check out the code dependencies. + +## General package dependencies (needed to compile Haskell services) + +*Note: all the below sections for getting compile-time dependencies necessary to compile all of wire-server may potentially go out of date; if you spot a mistake please open an issue or PR* + +### Nix + Direnv + +Using Stack's [Nix integration](https://docs.haskellstack.org/en/stable/nix_integration/), Stack will take care of installing any system +dependencies automatically - including `cryptobox-c`. If new system dependencies are needed, add them to the `stack-deps.nix` file in the project root. + +If you have `direnv` and `nix`, you will automatically have `make`, `docker-compose` and `stack` in `PATH` once you `cd` into the project root and `direnv allow`. +You can then run all the builds, and the native dependencies will be automatically present. + +1. Install [Nix](https://nixos.org/download.html) + * MacOS users with a recent Mac might need to follow [these + instructions](https://nixos.org/nix/manual/#sect-macos-installation) + * Debian users can use their distro's `nix` package, and should remember + + to add their user to the `nix-users` group in /etc/group, and re-start + their login session. +2. Install [Direnv](https://direnv.net/). + * On debian, you can install the `direnv` package. On MacOS use `brew install direnv`. + * On NixOS with home-manager, you can set `programs.direnv.enable = true;`. + * Make sure direnv is hooked into your shell via it's appripriate `rc` file. + Add `eval "$(direnv hook bash|zsh|fish)"` to your ~/.(bash|zsh|fish)rc . + * When successfully installed and hooked, direnv should ask you to `direnv allow` + the current `.envrc` when you cd to this repository. + See the [Installation documentation](https://direnv.net/docs/installation.html) for further details. + +### Fedora: + +```bash +sudo dnf install -y pkgconfig haskell-platform libstdc++-devel libstdc++-static gcc-c++ libtool automake openssl-devel libsodium-devel ncurses-compat-libs libicu-devel GeoIP-devel libxml2-devel snappy-devel protobuf-compiler +``` + +### Ubuntu / Debian: + +_Note_: Debian is not recommended due to this issue when running local integration tests: [#327](https://github.com/wireapp/wire-server/issues/327). This issue does not occur with Ubuntu. + +```bash +sudo apt install pkg-config libsodium-dev openssl-dev libtool automake build-essential libicu-dev libsnappy-dev libgeoip-dev protobuf-compiler libxml2-dev zlib1g-dev libtinfo-dev liblzma-dev libpcre3 libpcre3-dev -y +``` + +If `openssl-dev` does not work for you, try `libssl-dev`. + +### Arch: + +``` +# You might also need 'sudo pacman -S base-devel' if you haven't +# installed the base-devel group already. +sudo pacman -S geoip snappy icu openssl ncurses-compat-libs +``` + +### macOS: + +```bash +brew install pkg-config libsodium openssl automake icu4c geoip snappy protobuf +``` + +_Note_: macOS users will need to make sure to link Haskell services against a more recent version of OpenSSL than what ships with the OS by default. Additionally, `icu4c` is installed in a non-standard location by `homebrew`. Add the following to your `.stack/config.yaml`: + +```yaml +extra-include-dirs: +- /usr/local/opt/openssl/include +- /usr/local/opt/icu4c/include + +extra-lib-dirs: +- /usr/local/opt/openssl/lib +- /usr/local/opt/icu4c/lib +``` + +_Note_: if you're getting `fatal error: 'libxml/parser.h' file not found` and you're on macOS Mojave, try doing: + +```bash +sudo installer -pkg /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg -target / +``` + +## Haskell Stack + +Please refer to [Stack's installation instructions](https://docs.haskellstack.org/en/stable/README/#how-to-install). + +When you're done, ensure `stack --version` is the same as `STACK_VERSION` in [`build/ubuntu/Dockerfile.prebuilder`](../../build/ubuntu/Dockerfile.prebuilder). + +If you have to, you can downgrade stack with this command: + +```bash +stack upgrade --binary-version +``` + +### Ubuntu / Debian +_Note_: The packaged versions of `haskell-stack` are too old. It is recommended to follow the generic instructions or to use stack to update stack (`stack upgrade`). + +```bash +sudo apt install haskell-stack -y +``` + +### Generic + +```bash +curl -sSL https://get.haskellstack.org/ | sh +# or +wget -qO- https://get.haskellstack.org/ | sh +``` + +## Rust + +### Ubuntu / Debian +```bash +sudo apt install rustc cargo -y +``` + +### Generic + +```bash +curl https://sh.rustup.rs -sSf | sh +source $HOME/.cargo/env +``` + +## Formatting Haskell files + +You need [`ormolu`](https://github.com/tweag/ormolu) on your PATH, get it with `stack install ormolu` + +## Generating license headers + +We use [`headroom`](https://github.com/vaclavsvejcar/headroom), get it with `stack install headroom` + +## makedeb + +This is a tool to create debian-style binary packages. It is optional, and is only used if you want to install debian-style packages on your debian or ubuntu system. + +_Note_: If you want to build debian-style packages of cryptobox-c and other wire utilities, execute this step. otherwise, make sure to execute the 'Generic' version of the cryptobox-c step. + +```bash +git clone https://github.com/wireapp/wire-server && cd wire-server/tools/makedeb +export VERSION=0 +make dist +dpkg -i ../../dist/makedeb*.deb +``` + +## cryptobox-c + +### Ubuntu / Debian + +```bash +git clone https://github.com/wireapp/cryptobox-c && cd cryptobox-c +make dist +dpkg -i target/release/cryptobox*.deb +``` + +### Generic +```bash +export TARGET_LIB="$HOME/.wire-dev/lib" +export TARGET_INCLUDE="$HOME/.wire-dev/include" +mkdir -p "$TARGET_LIB" +mkdir -p "$TARGET_INCLUDE" +git clone https://github.com/wireapp/cryptobox-c && cd cryptobox-c +make install + +# Add cryptobox-c to ldconfig +sudo bash -c "echo \"${TARGET_LIB}\" > /etc/ld.so.conf.d/cryptobox.conf" +sudo ldconfig +``` + +Make sure stack knows where to find it. In `~/.stack/config.yaml` add: + +(using `~` or `$HOME` doesn't work, needs full paths) + +```yaml +extra-include-dirs: +- /usr/local/include +- /.wire-dev/include + +extra-lib-dirs: +- /usr/local/lib +- /.wire-dev/lib +``` + +## Docker + +_Note_: While it is possible to use non-docker solutions to set up and configure this software, we recommend using docker and our provided docker images to configure dependent services rapidly, and ensure a consistent environment for all potential developers. + +### Ubuntu / Debian Testing/Unstable: +```bash +sudo apt install docker.io docker-compose +``` + +After installing docker-io, add your user to the docker group, and restart your shell (usually involving a restart of your graphical environment). + +once you've logged in again, if you would like to upload any docker images (optional): +```bash +docker login --username= +``` + +### Generic: + +* [Install docker](https://docker.com) +* [Install docker-compose](https://docs.docker.com/compose/install/) + +## Telepresence + +You can instead use [telepresence](https://www.telepresence.io) to allow you to talk to services installed in a given kubernetes namespace on a local or remote kubernetes cluster using easy DNS names like: `curl http://elasticsearch:9200`. + +Requirements: + +* install telepresence (e.g. `nix-env -iA nixpkgs.telepresence`) +* you need access to a kubernetes cluster +* you need a namespace in which you have installed something (e.g. `make kube-integration-setup` will do this) + +### Telepresence example usage: + +``` +# terminal 1 +telepresence --namespace "$NAMESPACE" --also-proxy cassandra-ephemeral +``` + +``` +# terminal 2 +curl http://elasticsearch-ephemeral:9200 +``` + +### Telepresence example usage 2: + +``` +# just one terminal +telepresence --namespace "$NAMESPACE" --also-proxy cassandra-ephemeral --run bash -c "curl http://elasticsearch-ephemeral:9200" +``` + +### Telepresence usage discussion: + +* If you have `fake-aws` and `databases-ephemeral` helm charts set up, you can run either `brig` and other services locally (they connect to cassandra-inside-kubernetes) +* If you also have `brig` and other haskell services running in kubernetes (e.g. you ran `make kube-integration-setup`, you can use telepresence to only run test executables (like `brig-integration`) locally which connect to services inside kubernetes. + +In both cases, you need to adjust the various integration configuration files and names so that this can work. + +## Buildah (optional) + +[Buildah](https://buildah.io/) is used for local docker image creation during development. See [buildah installation](https://github.com/containers/buildah/blob/master/install.md) + +See `make buildah-docker` for an entry point here. + +## Helm chart development, integration tests in kubernetes + +You need `kubectl`, `helm`, `helmfile`, and a valid kubernetes context. Refer to https://docs.wire.com for details. diff --git a/docs/legacy/developer/editor-setup.md b/docs/legacy/developer/editor-setup.md new file mode 100644 index 00000000000..4e030f88b08 --- /dev/null +++ b/docs/legacy/developer/editor-setup.md @@ -0,0 +1,117 @@ +# Editor setup {#DevEditor} + +This page provides tips for setting up editors to work with the Wire codebase. + +## For multiple editors {#DevAll} + +### Using Haskell IDE Engine + +See [official documentation](https://github.com/haskell/haskell-ide-engine) + +In addition, you can generate (and re-generate after changes to stack.yaml) a `hie.yaml` configuration file with + +``` +make hie.yaml +``` + +## Emacs {#DevEmacs} + +### Jump-to-definition {#DevEmacsJump} + +Jump-to-definition is possible with [hasktags][]. First you need to install it and make sure it's on your PATH (if you don't want hasktags to be on your PATH, do `M-x customize-variable haskell-hasktags-path`): + +[hasktags]: https://hackage.haskell.org/package/hasktags + +```bash +stack install hasktags # or cabal install hasktags +``` + +To generate tags, do `M-x haskell-mode-generate-tags`. You can also add a Git hook to regenerate tags on checkout: + +```bash +echo "hasktags -e -x ." > .git/hooks/post-checkout +chmod +x .git/hooks/post-checkout +``` + +To jump to an identifier, press `M-.`. You can also do `C-u M-x xref-find-definitions` to get interactive search through identifiers. Press `M-,` to return to where you were before the jump. + +Jump-to-definition is case-insensitive by default, which is probably not what you want. To change that, do `M-x customize-variable tags-case-fold-search`. + +By default hasktags only generates tags for the current package. The Wire backend is composed of many packages, and it's useful to be able to jump to any identifier in wire-server. One way to do it is to setup Emacs to check if there's a Projectile project that the current directory belongs to, and if so, override the "current package" default. + +Install the [projectile][] package for Emacs and do `M-x projectile-add-known-project `. Then add the following snippet to your `init.el`: + +[projectile]: https://www.projectile.mx/en/latest/installation/ + +``` +(require 'haskell) +(require 'projectile) + +;; When inside a project, even if there is a cabal file in the current +;; folder, use the project folder to generate tags. This is useful for +;; projects with several services or subprojects. +(defadvice haskell-cabal--find-tags-dir (around project-tags act) + (setq ad-return-value + (if (projectile-project-root) + (projectile-project-root) + ad-do-it))) +``` + +### Haskell Language Server + +To use HLS bundled in direnv setup, here is a sample `.dir-locals.el` that can +be put in the root directory of the project: +```el +((haskell-mode . ((haskell-completion-backend . lsp) + (lsp-haskell-server-path . "/home/haskeller/code/wire-server/hack/bin/nix-hls.sh") + ))) +``` + +### Ormolu integration + +There are make targets `format`, `formatf`, `formatc` to re-format +or check the entire repository. This takes about 10 seconds. + +Emacs integration is [linked +here](https://github.com/tweag/ormolu#editor-integration). + +## Vim {#DevVim} + +### hie (Haskell IDE engine) integration + +* Generate `hie.yaml` as described [at the top of this file](#using-haskell-ide-engine) +* You may follow the setup described [in this blog post](http://marco-lopes.com/articles/Vim-and-Haskell-in-2019/) + +### ormolu integration + +There are make targets `format`, `formatf`, `formatc` to re-format +or check the entire repository. This takes about 10 seconds. + +Vim integration is [linked +here](https://github.com/tweag/ormolu#editor-integration). + +If you use sdiehl's module, you you need to collect the language extensions from `package-defaults.yaml`: + +``` + let g:ormolu_options = ["--ghc-opt -XAllowAmbiguousTypes --ghc-opt -XBangPatterns --ghc-opt -XConstraintKinds --ghc-opt -XDataKinds --ghc-opt -XDefaultSignatures --ghc-opt -XDerivingStrategies --ghc-opt -XDeriveFunctor --ghc-opt -XDeriveGeneric --ghc-opt -XDeriveLift --ghc-opt -XDeriveTraversable --ghc-opt -XEmptyCase --ghc-opt -XFlexibleContexts --ghc-opt -XFlexibleInstances --ghc-opt -XFunctionalDependencies --ghc-opt -XGADTs --ghc-opt -XInstanceSigs --ghc-opt -XKindSignatures --ghc-opt -XLambdaCase --ghc-opt -XMultiParamTypeClasses --ghc-opt -XMultiWayIf --ghc-opt -XNamedFieldPuns --ghc-opt -XNoImplicitPrelude --ghc-opt -XOverloadedStrings --ghc-opt -XPackageImports --ghc-opt -XPatternSynonyms --ghc-opt -XPolyKinds --ghc-opt -XQuasiQuotes --ghc-opt -XRankNTypes --ghc-opt -XScopedTypeVariables --ghc-opt -XStandaloneDeriving --ghc-opt -XTemplateHaskell --ghc-opt -XTupleSections --ghc-opt -XTypeApplications --ghc-opt -XTypeFamilies --ghc-opt -XTypeFamilyDependencies --ghc-opt -XTypeOperators --ghc-opt -XUndecidableInstances --ghc-opt -XViewPatterns"] +``` + +If you want to be playful, you can look at how `tools/ormolu.sh` +collects the language extensions automatically and see if you can get +it to work here. + +## VSCode + +The project can be loaded into the [Haskell Language Server](https://github.com/haskell/haskell-language-server). +This gives type checking, code completion, HLint hints, formatting with Ormolu, lookup of definitions and references, etc.. +All needed dependencies (like the `haskell-language-server` and `stack` binaries) are provided by `shell.nix`. + +Setup steps: +- Install the plugins `Haskell` (Haskell Language Server support), `Haskell Syntax` and `Nix Environment Selector` +- Generate the `hie.yaml` file: `make hie.yaml` +- Select the nix environment from `shell.nix` with the command `Nix-Env: Select environment`. +- Reload the window as proposed by the `Nix Environment Selector` plugin + +An alternative way to make these dependencies accessible to VSCode is to start it in the `direnv` environment. +I.e. from a shell that's current working directory is in the project. The drawbacks of this approach are +that it only works locally (not on a remote connection) and one VSCode process needs to be started per project. \ No newline at end of file diff --git a/docs/legacy/developer/features.md b/docs/legacy/developer/features.md new file mode 100644 index 00000000000..a07f505d4d3 --- /dev/null +++ b/docs/legacy/developer/features.md @@ -0,0 +1,77 @@ +## Features + +Wire has multiple features (or feature flags) that affect the behaviour of +backend and clients, e.g. `sso`, `searchVisibility`, `digitalSignatures`, +`appLock`, `legalhold`, `fileSharing`. + +Features cannot by configured by the users themselves, which distinguishes +features from user properties. Who can configure a feature depends on the +feature itself, e.g: + +- Team administrators via Wire Team Settings +- Customer support via the backoffice tool +- Site administrators by changing a configuration file on the server + +Depending on the feature the configuration can be defined + +- for all users on the server (per-instance) +- per-team +- or per-user + +or a combination of all three levels, where configuration on a per-user level +may override configuration defined for the the team, which may override +configuration defined at the instance level. or instance may override team +(eg. when security settings are enforced so team admins do not have the +power to break security policy). details depend on the individual feature. + +## Feature Configuration API + +The Feature configurations API exposes the feature configuration that results +from combining the settings (per-instance, per-team, per-user) for the user that +queries the endpoint: + +`GET /feature-configs/:feature-name` + +```json +{ + "status": "enabled" /* or "disabled" */ + "config": { + /* ... */ + } +} +``` +where the optional `config` field contains settings that are specific to the feature. + +The configurations for all features can be obtained all at once via `GET +/feature-configs` + +```json +{ + "sso": { + "status": "enabled" + }, + "searchVisibility": { + "status": "disabled" + }, + "appLock": { + "status": "enabled", + "config": { + "enforceAppLock": "true", + "inactivityTimeoutSecs": 300 + } + } + /* ... */ +} +``` + +Note: The implemenation of the Feature Configuration API is re-using the types +from `Wire.API.Team.Feature` (Team Features API). + +## Team Features API + +The Team features API preceedes the notion of the feature configuration API. The +endpoints of the form `GET /teams/:tid/features/:feature-name` and `PUT +/teams/:tid/features/:feature-name` can be used to get and set the feature +configuration on a per-team level. Features that cannot be get and set on a +per-team level may be missing from the Team Features API. See the Swagger +documentation on what endpoints are available. diff --git a/docs/legacy/developer/federation-api-conventions.md b/docs/legacy/developer/federation-api-conventions.md new file mode 100644 index 00000000000..84b73a8a6c0 --- /dev/null +++ b/docs/legacy/developer/federation-api-conventions.md @@ -0,0 +1,35 @@ + + +# Federation API Conventions + +- All endpoints must start with `/federation/` +- All path segments must be in kebab-case, and only consist of alphanumeric + characters. The name the field in the record must be the same name in + camelCase. +- There must be exactly one segment after `/federation/`, so + `/federation/foo` is valid, but `/federation/foo/bar` is not. +- All endpoints must be `POST`. +- No query query params or captured path params, all information that needs to + go must go in body. +- All responses must be `200 OK`, domain specific failures (e.g. the + conversation doesn't exist) must be indicated as a Sum type. Unhandled + failures can be 5xx, an endpoint not being implemented will of course + return 404. But we shouldn't pile onto these. This keeps the federator simple. +- Accept only json, respond with only json. Maybe we can think of changing + this in future. But as of now, the federator hardcodes application/json as + the content type of the body. +- Ensure that paths don't collide between brig and galley federation API, this + will be very helpful when we merge brig and galley. +- The name of the path segment after `/federation/` must be either + `-` or `on--`, e.g. + `get-conversations` or `on-conversation-created`. + + How to decide which one to use: + - If the request is supposed to ask for information/change from another + backend which has authority over that information, like send a message or + leave a conversation, then use the first format like `send-message` or + `leave-conversation`. + - If the request is supposed to notify a backend about something the caller of + this request has authority on, like a conversation got created, or a message + is sent, then use the second format like `on-conversation-created` or + `on-message-sent` diff --git a/docs/legacy/developer/how-to.md b/docs/legacy/developer/how-to.md new file mode 100644 index 00000000000..f259039cf9e --- /dev/null +++ b/docs/legacy/developer/how-to.md @@ -0,0 +1,206 @@ +# Developer how-to's + +The following assume you have a working developer environment with all the dependencies listed in [./dependencies.md](./dependencies.md) available to you. + +## How to look at the swagger docs / UI locally + +Terminal 1: +* Set up backing services: `./deploy/dockerephemeral/run.sh` + +Terminal 2: +* Compile all services: `make services` + * Note that you have to [import the public signing keys for nginx](../../services/nginz/README.md#common-problems-while-compiling) to be able to build nginz +* Run services including nginz: `export INTEGRATION_USE_NGINZ=1; ./services/start-services-only.sh` + +Open your browser at: + +- http://localhost:8080/api/swagger-ui for the swagger 2.0 endpoints (in development as of Feb 2021 - more endpoints will be added here as time goes on) +- http://localhost:8080/swagger-ui/ for the old swagger 1.2 API (old swagger, endpoints will disappear from here (and become available in the previous link) as time progresses) + +Swagger json (for swagger 2.0 endpoints) is available under http://localhost:8080/api/swagger.json + +## How to run federation tests across two backends + +Requirements: + +* `helm` and `kubectl` available on your PATH +* access to a kubernetes cluster via e.g. a `~/.kube/config` file. + +The process consists of: + +1. Inspect/change the multi-backend tests +2. Deploy two backends to kubernetes cluster +3. Run multi-backend test half-locally half-on-kubernetes or fully on kubernetes +4. Teardown + +### 1. Inspect/change the multi-backend test code + +Refer to `services/brig/test/integration/API/Federation/End2End.hs` for the current multi-backend tests. + +*Note that they only run if `INTEGRATION_FEDERATION_TESTS` is set to `1`. This is currently configured to be OFF when running regular brig integration tests (e.g. via `make -C services/brig integration`) but is by default ON when running tests on kubernetes or on CI, or when using the `services/brig/federation-tests.sh` script.* + +### 2. Deploy two backends to kubernetes cluster + +Decide which code you would like to deploy. The following options are detailed in the subsections below. + +* 2.1 Deploy the the latest compiled code from `develop` +* 2.2 Deploy code from your pull request +* 2.3 Deploy your local code to a kind cluster + +#### 2.1 Deploy the the latest compiled code from `develop` + +First, find the latest CI-compiled code made available as docker images: + +``` +# Run all commands from the top directory of wire-server +make latest-tag +``` + +Output might be + +``` +./hack/bin/find-latest-docker-tag.sh +latest tag for brig: +2.104.11 +latest tag for nginz: +2.104.11 +``` + +Let's assume the tags are the same(*) for both, then export an environment variable: + +``` +export DOCKER_TAG=2.104.11 +export NAMESPACE="myname" +make kube-integration-setup +``` + +This will create two full installations of wire-server on the kubernetes cluster you've configured to connect to, and should take ~10 minutes. The namespaces will be `$NAMESPACE` and `$NAMESPACE-fed2`. + + +##### Troubleshooting + +`make latest-tag` gives different tags for brig and nginz: + +* maybe CI hasn't finished, or failed. Look at concourse (`kubernetes-dev` pipeline) + +#### 2.2 Deploy code from your pull request + +*Note: CI already runs multi-backend federation integration tests on your PR, so this section may not be often useful in practice. This is still documented for completeness and to help understand the relation between source code and compiled docker images on CI.* + +Check CI for the latest tag that has been created on your PR (expect this to take at least 30-60 minutes from the last time you pushed to your branch). Example: + +Look at a successful job in the `wire-server-pr` pipeline from a job bruild matching your desired PR and commit hash. Then, find the actual docker tag used. + +![concourse-pr-version-circled](https://user-images.githubusercontent.com/2112744/114410146-69b34000-9bab-11eb-863c-106fb661ca82.png) + +``` +# PR 1438 commit 7a183b2dbcf019df1af3d3b97604edac72eca762 translates to +export DOCKER_TAG=0.0.1-pr.3684 +export NAMESPACE="myname" +make kube-integration-setup +``` + +#### 2.3 Deploy your local code to a kind cluster + +This can be useful to get quicker feedback while working on multi-backend code or configuration (e.g. helm charts) than to wait an hour for CI. This allows you to test code without uploading it to github and waiting an hour for CI. + +FUTUREWORK: this process is in development (update this section after it's confirmed to work): + +##### (i) Build images + +1. Ensure `buildah` is available on your system. +2. Compile the image using `make buildah-docker`. This will try to upload the + images into a `kind` cluster. If you'd prefer uploading images to quay.io, + you can run it with `make buildah-docker BUILDAH_PUSH=1 BUILDAH_KIND_LOAD=0` + +##### (ii) Run tests in kind + +0. Create a local kind cluster with `make kind-cluster` +1. Install wire-server using `make kind-integration-setup`. +2. Run tests using `make kind-integration-test`. +3. Run end2end integration tests: `make kind-integration-e2e`. + +NOTE: debug this process further as some images (e.g. nginz) are missing from the default buildah steps. +* Implement re-tagging development tags as your user tag? + +#### 2.4 Deploy your local code to a kubernetes cluster + +This sections describes how partially update a release with a local build of a service, in this example `brig`. + +Start by deploying a published release (see 2.1 or 2.2). + +``` +export NAMESPACE=$USER +export DOCKER_TAG=2.116.32 +make kube-integration-setup +``` + +Then build and push the `brig` image by running + +``` +export DOCKER_TAG_LOCAL_BUILD=$USER +hack/bin/buildah-compile.sh +DOCKER_TAG=$DOCKER_TAG_LOCAL_BUILD EXECUTABLES=brig BUILDAH_PUSH=1 ./hack/bin/buildah-make-images.sh +``` + +To update the release with brig's local image run +``` +./hack/bin/set-chart-image-version.sh "$DOCKER_TAG_LOCAL_BUILD" brig +./hack/bin/integration-setup-federation.sh +``` + + +## 3 Run multi-backend tests + +### Run all integration tests on kubernetes + +* takes ~10 minutes to run +* test output is delayed until all tests have run. You will have to scroll the output to find the relevant multi-backend test output. +* tests run entirely on kubernetes. +* includes running the federation multi-backend tests by default (see also section (1)) + +``` +make kube-integration-test +``` + +### Run only the multi-backend tests + +* runs faster (~ half a minute) +* test output is shown dynamically as tests run +* business logic code runs on kubernetes, but the test executable runs on your local computer (which connects using `telepresence`) + +1. ensure you have compiled brig-integration: `make -C services/brig fast` +2. ensure you have `telepresence` installed (see developer dependencies documentation) +3. Run the actual tests, (takes half a minute): + +``` +./services/brig/federation-tests.sh "$NAMESPACE" +``` + +Note that this runs your *locally* compiled `brig-integration`, so this allows to easily change test code locally with the following process: + +1. change code under `services/brig/test/integration/Federation/` +2. recompile: `make -C services/brig fast` +3. run `./services/brig/federation-tests.sh test-$USER` again. + +### Run selected integration tests on kuberentes + +To run selective tests from brig-integration: + +``` +helm -n $NAMESPACE get hooks $NAMESPACE-wire-server | yq '.' | jq -r 'select(.metadata.name | contains("brig-integration"))' > /tmp/integration-pod + +# optional: edit the test pattern /tmp/integration-pod + +kubectl apply -n $NAMESPACE -f /tmp/integration-pod +``` + +## 4 Teardown + +To destroy all the resources on the kubernetes cluster that have been created run + +``` +./hack/bin/integration-teardown-federation.sh +``` + +Note: Simply deleting the namespaces is insufficient, because it leaves some resources (of kind ClusterRole, ClusterRoleBinding) that cause problems when redeploying to the same namespace via helm. diff --git a/docs/legacy/developer/linting.md b/docs/legacy/developer/linting.md new file mode 100644 index 00000000000..5dcbf75ab0f --- /dev/null +++ b/docs/legacy/developer/linting.md @@ -0,0 +1,55 @@ +# Linting + +# HLint + +To run [HLint](https://github.com/ndmitchell/hlint) you need it's binary, e.g. +by executing: + +```sh +nix-shell -p hlint +``` + +To run it on the whole project (Warning: This takes a long time!): + +```sh +hlint . +``` + +To run it on a sub-project: + +```sh +hlint services/federator +``` + +# Stan + +To run [Stan](https://github.com/kowainik/stan), you need it's binary compiled +by the same GHC version as used in the project. + +```sh +nix-shell -p haskell.packages.ghc884.stan +``` + +Stan depends on [*hie*](https://www.haskell.org/ghc/blog/20190626-HIEFiles.html) +database files that are created during compilation. To generate them for all +packages add this to your `cabal.project.local` file: + +``` +package * + ghc-options: -fwrite-ide-info -hiedir=.hie +``` + +Of course, you can append the `ghc-options` to the respective entry of a package or +add a new one: + +```sh +package cargohold + ghc-options: -fwrite-ide-info -hiedir=.hie +``` + +To analyze a sub-project with stan: + +```sh +cd services/cargohold +stan +``` diff --git a/docs/legacy/developer/processes.md b/docs/legacy/developer/processes.md new file mode 100644 index 00000000000..4f1162c0c14 --- /dev/null +++ b/docs/legacy/developer/processes.md @@ -0,0 +1,50 @@ +# Internal processes + +The following processes only apply to Wire employees working on this code base. + +## Inspect helm charts before releasing them + +`make charts` will create a partial copy of the `./charts` folder under `./.local/charts/` and set helm chart versions as well as docker tags. + +`make chart-` can be used to copy & version-pin a single chart before its release. + +*See the `CHARTS` variable in the top-level Makefile for the current default list of charts.* + +## Run wire-server integration tests inside kubernetes using helm + +You need kubectl, helm, and a configured kubernetes cluster + +```sh +# for "whatever wire-server source code, usually latest develop, I don't care much" +export DOCKER_TAG=$(./hack/bin/find-latest-docker-tag.sh) +echo "$DOCKER_TAG" +# or, if you wish to test specific wire-server source code, e.g. a particular PR already built by CI: +export DOCKER_TAG= + +# The following can take ~15 minutes and will set up an ephemeral kubernetes namespace and run all integration tests. +make kube-integration +``` + +When you're done, you can run + +``` +make kube-integration-teardown +``` + +## Upload Helm charts to our S3 mirror + +Ideally, only CI will do this after commits are merged to develop or master respectively. If this needs to be done manually: + +Ensure you are either on develop or master, and your git working tree is clean. + +```sh +export HELM_SEMVER= # must be a semantic version +export DOCKER_TAG= + +# Upload all charts inside the makefile list: +make upload-charts + +# Upload a single chart, e.g. wire-server +make upload-wire-server +``` + diff --git a/docs/legacy/developer/scim/storage.md b/docs/legacy/developer/scim/storage.md new file mode 100644 index 00000000000..bcf64c64154 --- /dev/null +++ b/docs/legacy/developer/scim/storage.md @@ -0,0 +1,22 @@ +# Storing SCIM-related data {#DevScimStorage} + +_Author: Artyom Kazak, Matthias Fischmann_ + +--- + +## Storing user data {#DevScimStorageUsers} + +SCIM user data is validated by the spar service and stored as brig users. All fields that wire doesn't care about are silently dropped. `GET /scim/v2/Users` will trigger a lookup in brig, and the data thus obtained is synthesized back into a SCIM record. + +Time stamps `created_at` and `last_updated_at` for the SCIM metadata are stored in `spar.scim_user_times`. The are kept in sync with the users that are otherwise stored in brig. (Rationale: we briefly considered using `select writetime(*) from brig.user` for last update and `select writetime(activated) from brig.user` for creation, but this has a drawback: we don't have the time stamps when storing the record, so the `POST` handler would need to do a database write and a consecutive lookup, or an `insert if not exists`.) + +Users created by SCIM set the `ManagedBy` field in brig to `ManagedByScim`. This *should* lead to brig disallowing certain update operations (since the single source of truth should be the SCIM peer that has created and is updating the user), but we never got around to implementing that (as of Wed 15 Jul 2020 10:59:11 AM CEST). See also {@SparBrainDump} (grep for `ManagedBy`). + + +## Storing SCIM tokens {#DevScimStorageTokens} + +[SCIM tokens](../../reference/provisioning/scim-token.md) are stored in two tables in Spar: + +* `team_provisioning_by_token` for `token -> token info` lookups; used to perform authentication. + +* `team_provisioning_by_team` for `team -> [token info]` and `(team, token ID) -> token info` lookups; used to display tokens in team settings, and to decide which tokens should be deleted when the whole team is deleted. diff --git a/docs/legacy/developer/servant.md b/docs/legacy/developer/servant.md new file mode 100644 index 00000000000..0b577f7f2cf --- /dev/null +++ b/docs/legacy/developer/servant.md @@ -0,0 +1,58 @@ +# Introduction + +We currently use Servant for the public (i.e. client-facing) API in brig, galley and spar, as well as for their federation (i.e. server-to-server) and internal API. + +Client-facing APIs are defined in `Wire.API.Routes.Public.{Brig,Galley}`. Internal APIs are all over the place at the moment. Federation APIs are in `Wire.API.Federation.API.{Brig,Galley}`. + +Our APIs are able to generate Swagger documentation semi-automatically using `servant-swagger2`. The `schema-profunctor` library (see [`README.md`](/libs/schema-profunctor/README.md) in `libs/schema-profunctor`) is used to create "schemas" for the input and output types used in the Servant APIs. A schema contains all the information needed to serialise/deserialise JSON values, as well as the documentation and metadata needed to generate Swagger. + +# Combinators + +We have employed a few custom combinators to try to keep HTTP concerns and vocabulary out of the API handlers that actually implement the functionality of the API. + +## `ZAuth` + +This is a family of combinators to handle the headers that nginx adds to requests. We currently have: + + - `ZUser`: extracts the `UserId` in the `Z-User` header. + - `ZLocalUser`: same as `ZUser`, but as a `Local` object (i.e. qualified by the local domain); this is useful when writing federation-aware handlers. + - `ZConn`: extracts the `ConnId` in the `Z-Connection` header. + - `ZConversation`: extracts the `ConvId` in the `Z-Conversation` header. + +## `MultiVerb` + +This is an alternative to `UVerb`, designed to prevent any HTTP-specific information from leaking into the type of the handler. Use this for endpoints that can return multiple responses. + +## `CanThrow` + +This can be used to add an error response to the Swagger documentation. In services that use polysemy for error handling (currently only Galley), it also adds a corresponding error effect to the type of the handler. The argument of `CanThrow` can be of a custom kind, usually a service-specific error kind (such as `GalleyError`, `BrigError`, etc...), but kind `*` can also be used. + +Note that error types can also be turned into `MultiVerb` responses using the `ErrorResponse` combinator. This is useful for handlers that can return errors as part of their return type, instead of simply throwing them as IO exceptions or using polysemy. If an error is part of `MultiVerb`, there is no need to also report it with `CanThrow`. + +## `QualifiedCapture` + +This is a capture combinator for a path that looks like `/:domain/:value`, where `value` is of some arbitrary type `a`. The value is returned as a value of type `Qualified a`, which can then be used in federation-aware endpoints. + +# Error handling + +Several layers of error handling are involved when serving a request. A handler in service code (e.g. Brig or Galley) can: + + 1. return a value on the `Right`; + 2. return a value on the `Left`; + 3. throw an `IO` exception. + +The `Handler → Servant.Handler` function, together with Servant's internal response creation logic, will, respectively: + + 1. produce a normal response; + 2. produce an error response, possibly logging the error; + 3. (ignore any `IO` exceptions, and let them bubble up). + +Finally, the error-catching middleware `catchErrors` in `Network.Wai.Utilities.Server` will: + + 1. let normal responses through; + 2. depending on the status code: + - if < 500, let the error through; + - if >= 500, wrap the error response in a JSON error object (if it is not already + one), and log it at error level. + 3. catch the exception, turn it into a JSON error object, and log it. The + log level depends on the status code (error for 5xx, debug otherwise). diff --git a/docs/legacy/developer/testing.md b/docs/legacy/developer/testing.md new file mode 100644 index 00000000000..0e41f8920bf --- /dev/null +++ b/docs/legacy/developer/testing.md @@ -0,0 +1,25 @@ +## Testing the wire-server Haskell code base + +Every package in this repository should have one or more directories +named `test` or `test*`. Ideally, there are at least unit tests +(eg. in `test/unit`) for libraries, and and at least unit and +integration tests (eg. `test/integration`) for packages with +executables (`/services`). + +### General rule + +Write as much pure code as possible. If you write a function that has +an effect only in a small part of its implementation, write a pure +function instead, and call if from an effectful function. + +### Unit tests + +All data types that are serialized ([`ToByteString`](https://hackage.haskell.org/package/amazonka-core-1.6.1/docs/Network-AWS-Data-ByteString.html#t:ToByteString), [`ToByteString`](https://hackage.haskell.org/package/aeson/docs/Data-Aeson.html#t:ToJSON), swagger, cassandra, ...) should have roundtrip quickcheck tests like [here](https://github.com/wireapp/wire-server/blob/develop/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs#L235). +All pure functions `f` that do something interesting should have a couple of tests of the form `shouldBe (f ) ` to cover corner cases. +If code is refactored, all the effects should be mocked with polysemy or mtl, the old implementation should be moved to the test suite, and there should be quickcheck tests running the new implementation against the old and comparing results ([example](https://github.com/wireapp/wire-server/blob/develop/services/gundeck/test/unit/MockGundeck.hs)). + +### Integration tests + +- All new rest API end-points need to be called a few times to cover as much of the corner cases as feasible. +- We have machinery to set up scaffolding teams and modify context such as server configuration files where needed. Look through the existing code for inspiration. +- Test only semantics, not syntax: do not post verbatim json to an end-point, but use the Haskell data types to render the json that you post. diff --git a/docs/legacy/reference/cassandra-schema.cql b/docs/legacy/reference/cassandra-schema.cql new file mode 120000 index 00000000000..0939aa54a6e --- /dev/null +++ b/docs/legacy/reference/cassandra-schema.cql @@ -0,0 +1 @@ +../../../cassandra-schema.cql \ No newline at end of file diff --git a/docs/legacy/reference/config-options.md b/docs/legacy/reference/config-options.md new file mode 100644 index 00000000000..2b30e07ac85 --- /dev/null +++ b/docs/legacy/reference/config-options.md @@ -0,0 +1,493 @@ +# Config Options {#RefConfigOptions} + +Fragment. + +This page is about the yaml files that determine the configuration of +the Wire backend services. + +## Settings in galley + +``` +# [galley.yaml] +settings: + enableIndexedBillingTeamMembers: false +``` + +### Indexed Billing Team Members + +Use indexed billing team members for journaling. When `enabled`, +galley would use the `billing_team_member` table to send billing +events with user ids of team owners (who have the `SetBilling` +permission). Before enabling this flag, the `billing_team_member` +table must be backfilled. + +Even when the flag is `disabled`, galley will keep writing to the +`biling_team_member` table, this flag only affects the reads and has +been added in order to deploy new code and backfill data in +production. + +## Feature flags + +Feature flags can be used to turn features on or off, or determine the +behavior of the features. Example: + +``` +# [galley.yaml] +settings: + featureFlags: + sso: disabled-by-default + legalhold: disabled-by-default + teamSearchVisibility: disabled-by-default + setEmailVisibility: visible_to_self +``` + +The `featureFlags` field in the galley settings is mandatory, and all +features must be listed. Each feature defines its own set of allowed +flag values. (The reason for that is that as we will see, the +semantics is slightly different (or more specific) than boolean.) + +### SSO + +This sets the default setting for all teams, and can be overridden by +customer support / backoffice. [Allowed +values](https://github.com/wireapp/wire-server/blob/46713382a1a6544de3936eb03e987b9f76df3faa/libs/galley-types/src/Galley/Types/Teams.hs#L327-L329): +`disabled-by-default`, `enabled-by-default`. + +IMPORTANT: if you change this from 'enabled-by-default' to +'disabled-by-default' in production, you need to run [this migration +script](https://github.com/wireapp/wire-server/tree/master/tools/db/migrate-sso-feature-flag) +to fix all teams that have registered an idp. (if you don't, the idp +will keep working, but the admin won't be able to register new idps.) + +### LegalHold + +Optionally block customer support / backoffice from enabling legal +hold for individual teams. [Allowed +values](https://github.com/wireapp/wire-server/blob/46713382a1a6544de3936eb03e987b9f76df3faa/libs/galley-types/src/Galley/Types/Teams.hs#L332-L334): +'disabled-permanently', 'disabled-by-default'. + +IMPORTANT: If you switch this back to `disabled-permanently` from +`disabled-by-default`, LegalHold devices may still be active in teams +that have created them while it was allowed. This may change in the +future. + +### Team Feature teamSearchVisibility + +The feature flag `teamSearchVisibility` affects the outbound search of user +searches. If it is set to `no-name-outside-team` for a team then all users of +that team will no longer be able to find users that are not part of their team +when searching. This also includes finding other users by by providing their +exact handle. By default it is set to `standard`, which doesn't put any +additional restrictions to outbound searches. + +The setting can be changed via endpoint: + +``` +GET /teams/{tid}/search-visibility + -- Shows the current TeamSearchVisibility value for the given team + +PUT /teams/{tid}/search-visibility + -- Set specific search visibility for the team + +pull-down-menu "body": + "standard" + "no-name-outside-team" +``` + +The default setting that applies to all teams on the instance can be defined at configuration + +```yaml +settings: + featureFlags: + teamSearchVisibility: disabled-by-default # or enabled-by-default +``` + +where disabled is equivalent to `standard` and enabled is equivalent to `no-name-outside-team`. Individual teams may ovewrite the default setting. + +On wire cloud the default setting is `standard`. + +### TeamFeature searchVisibilityInbound + +The team feature flag `searchVisibilityInbound` affects if the team's users are +searchable by users from _other_ teams. The default setting is +`searchable-by-own-team` which hides users from search results by users from +other teams. If it is set to `searchable-by-all-teams` then users of this team +may be included in the results of search queries by other users. + +Note: The configuration of this flag does _not_ affect search results when the +search query matches the handle exactly. If the handle is provdided then any user on the instance can find users. + +This team feature flag can only by toggled by site-administrators with direct access to the galley instance: + +``` +PUT /i/teams/{tid}/features/search-visibility-inbound +with JSON body {"status": "enabled"} or body {"status": disabled} +``` + +where `enabled` is equivalent to `searchable-by-all-teams` and disabled is equivalent to `searchable-by-own-team`. + +The default setting that applies to all teams on the instance can be defined at configuration. + +```yaml +searchVisibilityInbound: + defaults: + status: enabled # OR disabled +``` + +Individual teams can overwrite the default setting. + +### Email Visibility + +[Allowd values](https://github.com/wireapp/wire-server/blob/0126651a25aabc0c5589edc2b1988bb06550a03a/services/brig/src/Brig/Options.hs#L304-L306) and their [description](https://github.com/wireapp/wire-server/blob/0126651a25aabc0c5589edc2b1988bb06550a03a/services/brig/src/Brig/Options.hs#L290-L299). + +### Classified domains + +To enable classified domains, the following needs to be in galley.yaml or wire-server/values.yaml under `settings` / `featureFlags`: + +```yaml +classifiedDomains: + status: enabled + config: + domains: ["example.com", "example2.com"] +``` + +Note that when enabling this feature, it is important to provide your own domain +too in the list of domains. In the example above, `example.com` or `example2.com` is your domain. + +To disable, either omit the entry entirely (it is disabled by default), or provide the following: + +```yaml +classifiedDomains: + status: disabled + config: + domains: [] +``` + +### Conference Calling + +The `conferenceCalling` feature flag controls whether a user can initiate a conference call. The flag can be toggled between its states `enabled` and `disabled` per team via an internal endpoint. + +The `conferenceCalling` section in `featureFlags` defines the state of the `conferenceCalling` feature flag for all personal users (users that don't belong to a team). For personal users there is no way to toggle the flag, so the setting of the config section wholly defines the state of `conferenceCalling` flag for all personal users. + +The `conferenceCalling` section in `featureFlags` also defines the _initial_ state of the `conferenceCalling` flag for all teams. After the flag is set for the first time for a team via the internal endpoint the value from the config section will be ignored. + +Example value for the config section: +```yaml +conferenceCalling: + defaults: + status: enabled +``` + +The `conferenceCalling` section is optional in `featureFlags`. If it is omitted then it is assumed to be `enabled`. + +See also: conference falling for personal accounts (below). + +### File Sharing + +File sharing is enabled and unlocked by default. If you want a different configuration, use the following syntax: + +```yaml +fileSharing: + defaults: + status: disabled|enabled + lockStatus: locked|unlocked +``` + +These are all the possible combinations of `status` and `lockStatus`: + +| `status` | `lockStatus` | | +| ---------- | ------------ | ------------------------------------------------- | +| `enabled` | `locked` | Feature enabled, cannot be disabled by team admin | +| `enabled` | `unlocked` | Feature enabled, can be disabled by team admin | +| `disabled` | `locked` | Feature disabled, cannot be enabled by team admin | +| `disabled` | `unlocked` | Feature disabled, can be enabled by team admin | + +The lock status for individual teams can be changed via the internal API (`PUT /i/teams/:tid/features/fileSharing/(un)?locked`). + +The feature status for individual teams can be changed via the public API (if the feature is unlocked). + +### Validate SAML Emails + +If this is enabled, if a new user account is created with an email address as SAML NameID or SCIM externalId, users will receive a validation email. If they follow the validation procedure, they will be able to receive emails about their account, eg., if a new device is associated with the account. If the user does not validate their email address, they can still use it to login. + +Validate SAML emails is enabled by default; this is almost always what you want. If you want a different configuration, use the following syntax: + +```yaml +# galley.yaml +validateSAMLEmails: + defaults: + status: disabled +``` + +### 2nd Factor Password Challenge + +By default Wire enforces a 2nd factor authentication for certain user operations like e.g. activating an account, changing email or password, or deleting an account. +If this feature is enabled, a 2nd factor password challenge will be performed for a set of additional user operations like e.g. for generating SCIM tokens, login, or adding a client. + +Usually the default is what you want. If you explicitly want to enable the feature, use the following syntax: + +```yaml +# galley.yaml +sndFactorPasswordChallenge: + defaults: + status: disabled|enabled + lockStatus: locked|unlocked +``` + +### Federation Domain + +Regardless of whether a backend wants to enable federation or not, the operator +must decide what its domain is going to be. This helps in keeping things +simpler across all components of Wire and also enables to turn on federation in +the future if required. + +For production uses, it is highly recommended that this domain be configured as +something that is controlled by the operator(s). The backend or frontend do not +need to be available on this domain. As per our current federation design, you +must be able to set an SRV record for `_wire-server-federator._tcp.`. +This record should have entries which lead to the federator. + +**IMPORTANT** Once this option is set, it cannot be changed without breaking +experience for all the users which are already using the backend. + +This configuration needs to be made in brig, cargohold and galley (note the +slighly different spelling of the config options). + +```yaml +# brig.yaml +optSettings: + setFederationDomain: example.com +``` + +```yaml +# cargohold.yaml +settings: + federationDomain: example.com +``` + +```yaml +# galley.yaml +settings: + federationDomain: example.com +``` + +### Federation allow list + +As of 2021-07, federation (whatever is implemented by the time you read this) is turned off by default by means of having an empty allow list: + +```yaml +# federator.yaml +optSettings: + federationStrategy: + allowedDomains: [] +``` + +You can choose to federate with a specific list of allowed servers: + + +```yaml +# federator.yaml +optSettings: + federationStrategy: + allowedDomains: + - server1.example.com + - server2.example.com +``` + +or, you can federate with everyone: + +```yaml +# federator.yaml +optSettings: + federationStrategy: + # note the 'empty' value after 'allowAll' + allowAll: + +# when configuring helm charts, this becomes (note 'true' after 'allowAll') +# inside helm_vars/wire-server: +federator: + optSettings: + federationStrategy: + allowAll: true +``` + +### Federation TLS Config + +When a federator connects with another federator, it does so over HTTPS. There +are a few options to configure the CA for this: +1. `useSystemCAStore`: Boolean. If set to `True` it will use the system CA. +2. `remoteCAStore`: Maybe Filepath. This config option can be used to specify + multiple certificates from either a single file (multiple PEM formatted + certificates concatenated) or directory (one certificate per file, file names + are hashes from certificate). +3. `clientCertificate`: Maybe Filepath. A client certificate to use when + connecting to remote federators. If this option is omitted, no client + certificate is used. If it is provided, then the `clientPrivateKey` option + (see below) must be provided as well. +4. `clientPrivateKey`: Maybe Filepath. The private key corresponding to the + `clientCertificate` option above. It is an error to provide only a private key + without the corresponding certificate. + +Both the `useSystemCAStore` and `remoteCAStore` options can be specified, in +which case the stores are concatenated and used for verifying certificates. +When `useSystemCAStore` is set to `false` and `remoteCAStore` is not provided, +all outbound connections will fail with a TLS error as there will be no CA for +verifying the server certificate. + +#### Examples + +Federate with anyone, no client certificates, use system CA store to verify +server certificates: + +```yaml +federator: + optSettings: + federationStrategy: + allowAll: + useSystemCAStore: true +``` + +Federate only with `server2.example.com`, use a client certificate and a +specific CA: + +```yaml +federator: + optSettings: + federationStrategy: + allowedDomains: + - server2.example.com + useSystemCAStore: false + clientCertificate: client.pem + clientPrivateKey: client-key.pem +``` + +## Settings in brig + +Some features (as of the time of writing this: only +`conferenceCalling`) allow to set defaults for personal accounts in +brig. Those are taken into account in galley's end-points `GET +/feature-configs*`. + +To be specific: + +### Conference Calling + +Two values can be configured for personal accounts: a default for when +the user record contains `null` as feature config, and default that +should be inserted into the user record when creating new users: + +``` +# [brig.yaml] +settings: + setFeatureFlags: + conferenceCalling: + defaultForNew: + status: disabled + defaultForNull: + status: enabled +``` + +You can omit the entire `conferenceCalling` block, but not parts of +it. Built-in defaults: `defaultForNew: null` (user record attribute +is left empty); `defaultForNull: enabled`. This maintains behavior +prior to the introduction of this change, while allowing site owners +to postpone the decision about the default setting. + +When new users are created, their config will be initialized with +what's in `defaultForNew`. + +When a `null` value is encountered, it is assumed to be +`defaultForNull`. + +(Introduced in https://github.com/wireapp/wire-server/pull/1811.) + +### SFT configuration + +Configuring SFT load balancing can be done in two (mutually exclusive) settings: + +1) Configuring a SRV DNS record based load balancing setting + +``` +# [brig.yaml] +sft: + sftBaseDomain: sft.wire.example.com + sftSRVServiceName: sft + sftDiscoveryIntervalSeconds: 10 + sftListLength: 20 +``` + +or + +2) Configuring a HTTP-based load balancing setting + +``` +# [brig.yaml] +settings: + setSftStaticUrl: https://sft.wire.example.com +``` + +This setting assumes that the sft load balancer has been deployed with the `sftd` helm chart. + +Additionally if `setSftListAllServers` is set to `enabled` (disabled by default) then the `/calls/config/v2` endpoint will include a list of all servers that are load balanced by `setSftStaticUrl` at field `sft_servers_all`. This is required to enable calls between federated instances of Wire. + +### Locale + + +#### setDefaultLocale (deprecated / ignored) + +The brig server config option `setDefaultLocale` has been replaced by `setDefaultUserLocale` and `setDefaultTemplateLocale`. Both settings are optional and `setDefaultTemplateLocale` defaults to `EN` and `setDefaultLocale` defaults to `setDefaultTemplateLocale`. If `setDefaultLocale` was not set or set to `EN` before this change, nothing needs to be done. If `setDefaultLocale` was set to any other language other than `EN` the name of the setting should be changed to `setDefaultTemplateLocale`. + +#### `setDefaultTemplateLocale` + +This option determines the default locale for email templates. The language of the email communication is determined by the user locale (see above). Only if templates of the the locale of the user do not exist or if user locale is not set the `setDefaultTemplateLocale` is used as a fallback. If not set the default is `EN`. This setting should not be changed unless a complete set of templates is available for the given language. + +``` +# [brig.yaml] +optSettings: + setDefaultTemplateLocale: en +``` + +#### `setDefaultUserLocale` + +This option determines which language to use for email communication. It is the default value if none is given in the user profile, or if no user profile exists (eg., if user is being provisioned via SCIM or manual team invitation via the team management app). If not set, `setDefaultTemplateLocale` is used instead. + +``` +# [brig.yaml] +optSettings: + setDefaultUserLocale: en +``` + +### MLS settings + +#### `setKeyPackageMaximumLifetime` + +This option specifies the maximum accepted lifetime of a key package from the moment it is uploaded, in seconds. For example, when brig is configured as follows: + +``` +# [brig.yaml] +optSettings: + setKeyPackageMaximumLifetime: 1296000 # 15 days +``` + +any key package whose expiry date is set further than 15 days after upload time will be rejected. + + +### Federated domain specific configuration settings +#### Restrict user search + +The lookup and search of users on a wire instance can be configured. This can be done per federated domain. + +```yaml +# [brig.yaml] +optSettings: + setFederationDomainConfigs: + - domain: example.com + search_policy: no_search +``` + +Valid values for `search_policy` are: +- `no_search`: No users are returned by federated searches. +- `exact_handle_search`: Only users where the handle exactly matches are returned. +- `full_search`: Additionally to `exact_handle_search`, users are found by a freetext search on handle and display name. + +If there is no configuration for a domain, it's defaulted to `no_search`. diff --git a/docs/legacy/reference/conversation.md b/docs/legacy/reference/conversation.md new file mode 100644 index 00000000000..1d4af8569e6 --- /dev/null +++ b/docs/legacy/reference/conversation.md @@ -0,0 +1,142 @@ +# Creating and populating conversations {#RefCreateAndPopulateConvs} + +_Author: Matthias Fischmann_ + +--- + +This will walk you through creating and populating a conversation +using [curl](https://curl.haxx.se/) commands and the credentials of an +ordinary user (member role). + +If you have a system for identity management like a SAML IdP, you may +be able to use that as a source of user and group information, and +write a script or a program based on this document to keep your wire +conversations in sync with your groups. + +Sidenote: in the future we may implement groups in our [SCIM +API](http://www.simplecloud.info/) to handle conversations. For the +time being, we hope you consider the approach explained here a decent +work-around. + + +## Prerequisites + +We will talk to the backend using the API that the clients use, so we +need a pseudo-user that we can authenticate as. We assume this user +has been created by other means. For the sake of testing, you could +just use the team admin (but you don't need admin privileges for this +user). + +So here is some shell environment we will need: + +```bash +export WIRE_BACKEND=https://prod-nginz-https.wire.com +export WIRE_USER=... +export WIRE_PASSWD=... +export WIRE_TEAMID=... +``` + +Now you can login and get a wire token to authenticate all further +requests: + +```bash +export BEARER=$(curl -X POST \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json' \ + -d '{"email":"'"$WIRE_USER"'","password":"'"$WIRE_PASSWD"'"}' \ + $WIRE_BACKEND/login'?persist=false' | jq -r .access_token) +``` + +This token will be good for 15 minutes; after that, just repeat. + +If you don't want to install [jq](https://stedolan.github.io/jq/), you +can just call the `curl` command and copy the access token into the +shell variable manually. + +Here is a quick test that you're logged in: + +```bash +curl -X GET --header "Authorization: Bearer $BEARER" \ + $WIRE_BACKEND/self +``` + + +## Contact requests + +If `$WIRE_USER` is in a team, all other team members are implicitly +connected to it. So for the users in your team, you don't have to do +anything here. + +TODO: contact requests to users not on the team. + + +## Conversations + +To create a converation with no users (except the pseudo-user creating it): + +```bash +export WIRE_CONV_NAME="The B-Team" +export WIRE_CONV='{ + "users": [], + "name": "'${WIRE_CONV_NAME}'", + "team": { + "managed": false, + "teamid": "'${WIRE_TEAMID}'" + }, + "receipt_mode": 0, + "message_timer": 0 +}' + +export CONV_ID=$(curl -X POST --header "Authorization: Bearer $BEARER" \ + -H "Content-Type: application/json" \ + -d "$WIRE_CONV" \ + $WIRE_BACKEND/conversations | jq -r .id) +``` + +The `users` field can contain UUIDs that need to point to existing +wire users (in your team or not), and `$WIRE_USER` needs to have an +accepted connection with them. You can extract these ids from the +corresponding SCIM user records. If in doubt, leave empty. + +You can also add and remove users once the converation has been +created: + +```bash +curl -X POST --header "Authorization: Bearer $BEARER" \ + -H "Content-Type: application/json" \ + -d '{ "users": ["b4b6a96c-70c8-11e9-99e6-f3ea044b132c", "b7293854-70c8-11e9-b620-97ff1eba6324"] }' \ + $WIRE_BACKEND/conversations/$CONV_ID/members + +curl -X DELETE --header "Authorization: Bearer $BEARER" \ + $WIRE_BACKEND/conversations/$CONV_ID/members/b9f1c786-70c8-11e9-91a6-fbeb48cdcdd1 +``` + +You can also look at one or all conversations: + +```bash +curl -X GET --header "Authorization: Bearer $BEARER" \ + $WIRE_BACKEND/conversations/ids + +curl -X GET --header "Authorization: Bearer $BEARER" \ + $WIRE_BACKEND/conversations/ + +curl -X GET --header "Authorization: Bearer $BEARER" \ + $WIRE_BACKEND/conversations/$CONV_ID/ +``` + +Finally, conversations can be renamed or deleted: + +```bash +curl -X PUT --header "Authorization: Bearer $BEARER" \ + -H "Content-Type: application/json" \ + -d '{ "name": "The C-Team" }' \ + $WIRE_BACKEND/conversations/$CONV_ID + +curl -X DELETE --header "Authorization: Bearer $BEARER" \ + $WIRE_BACKEND/teams/$WIRE_TEAMID/conversations/$CONV_ID +``` + + +## Advanced topics + +TODO: pseudo-user leaving the conv, and being added by admin for changes later. diff --git a/docs/legacy/reference/elastic-search.md b/docs/legacy/reference/elastic-search.md new file mode 100644 index 00000000000..246988ec014 --- /dev/null +++ b/docs/legacy/reference/elastic-search.md @@ -0,0 +1,241 @@ +# Maintaining ElasticSearch + +## Update mapping + +```bash +ES_HOST= +ES_PORT= # default is 9200 +ES_INDEX= # default is directory +WIRE_VERSION= + +docker run "quay.io/wire/brig-index:$WIRE_VERSION" update-mapping \ + --elasticsearch-server "http://$ES_HOST:$ES_PORT" \ + --elasticsearch-index "$ES_INDEX" +``` + +Instead of running this in docker, this can also be done by building the `brig-index` binary from `services/brig` and executing it like this: + +```bash +brig-index update-mapping \ + --elasticsearch-server "http://$ES_HOST:$ES_PORT" \ + --elasticsearch-index "$ES_INDEX" +``` + +## Migrate Data + +```bash +ES_HOST= +ES_PORT= # default is 9200 +ES_INDEX= # default is directory +BRIG_CASSANDRA_HOST= +BRIG_CASSANDRA_PORT= +BRIG_CASSANDRA_KEYSPACE= +WIRE_VERSION= +GALLEY_HOST= +GALLEY_PORT= + +docker run "quay.io/wire/brig-index:$WIRE_VERSION" migrate-data \ + --elasticsearch-server "http://$ES_HOST:$ES_PORT" \ + --elasticsearch-index "$ES_INDEX" \ + --cassandra-host "$BRIG_CASSANDRA_HOST" \ + --cassandra-port "$BRIG_CASSANDRA_PORT" \ + --cassandra-keyspace "$BRIG_CASSANDRA_KEYSPACE" + --galley-host "$GALLEY_HOST" + --galley-port "$GALLEY_PORT" +``` + +(Or, as above, you can also do the same thing without docker.) + +## Refill ES documents from Cassandra + +This is needed if the information we keep in elastic search increases. +Also update the indices. + +```bash +ES_HOST= +ES_PORT= # default is 9200 +ES_INDEX= # default is directory +BRIG_CASSANDRA_HOST= +BRIG_CASSANDRA_PORT= +BRIG_CASSANDRA_KEYSPACE= +WIRE_VERSION= +GALLEY_HOST= +GALLEY_PORT= + +docker run "quay.io/wire/brig-index:$WIRE_VERSION" reindex \ + --elasticsearch-server "http://$ES_HOST:$ES_PORT" \ + --elasticsearch-index "$ES_INDEX" \ + --cassandra-host "$BRIG_CASSANDRA_HOST" \ + --cassandra-port "$BRIG_CASSANDRA_PORT" \ + --cassandra-keyspace "$BRIG_CASSANDRA_KEYSPACE" + --galley-host "$GALLEY_HOST" + --galley-port "$GALLEY_PORT" +``` + +Subcommand `reindex-if-same-or-newer` can be used instead of `reindex`, if you want to recreate the documents in elasticsearch regardless of their version. + +(Or, as above, you can also do the same thing without docker.) + +## Migrate to a new index + +This is needed if we want to migrate to a new index. It could be for updating +analysis settings or to change any other settings on the index which cannot be +done without restarting the index. Analysis settings can also be updated by +recreating the index. Recreating the index is simpler to do, but requires +downtime, the process is documented [below](#recreate-an-index-requires-downtime) + +This can be done in 4 steps: + +Before starting, please set these environment variables +```bash +ES_HOST= +ES_PORT= # default is 9200 +ES_SRC_INDEX= +ES_DEST_INDEX= +WIRE_VERSION= +SHARDS= +REPLICAS= +REFRESH_INTERVAL= +``` + +1. Create the new index + ```bash + docker run "quay.io/wire/brig-index:$WIRE_VERSION" create \ + --elasticsearch-server "http://$ES_HOST:$ES_PORT" \ + --elasticsearch-index "$ES_DEST_INDEX" \ + --elasticsearch-shards "$SHARDS" \ + --elasticsearch-replicas "$REPLICAS" \ + --elasticsearch-refresh-interval "$REFRESH_INTERVAL" + ``` +1. Redeploy brig with `elasticsearch.additionalWriteIndex` set to the name of new index. Make sure no old brigs are running. +1. Reindex data to the new index + ```bash + docker run "quay.io/wire/brig-index:$WIRE_VERSION" reindex-from-another-index \ + --elasticsearch-server "http://$ES_HOST:$ES_PORT" \ + --source-index "$ES_SRC_INDEX" \ + --destination-index "$ES_DEST_INDEX" + ``` + Optionally, `--timeout ` can be added to increase/decrease from the default timeout of 10 minutes. +1. Redeploy brig without `elasticsearch.additionalWriteIndex` and with `elasticsearch.index` set to the name of new index + +Now you can delete the old index. + +**NOTE**: There is a bug hidden when using this way. Sometimes a user won't get +deleted from the index. Attempts at reproducing this issue in a simpler +environment have failed. As a workaround, there is a tool in +[tools/db/find-undead](../../tools/db/find-undead) which can be used to find the +undead users right after the migration. If they exist, please run refill the ES +documents from cassandra as described [above](#refill-es-documents-from-cassandra) + +## Migrate to a new cluster + +If the ES cluster used by brig needs to be shutdown and data must be moved to a +new cluser, these steps can be taken to ensure minimal disruption to the +service. + +Before starting, please set these environment variables: + +```bash +ES_OLD_HOST= +ES_OLD_PORT= # usually 9200 +ES_OLD_INDEX= +ES_NEW_HOST= +ES_NEW_PORT= # usually 9200 +ES_NEW_INDEX= +WIRE_VERSION= +GALLEY_HOST= +GALLEY_PORT= + +# Use curl http://$ES_OLD_HOST:$ES_OLD_PORT/$ES_OLD_INDEX/_settings +# to know previous values of SHARDS, REPLICAS and REFRESH_INTERVAL +SHARDS= +REPLICAS= +REFRESH_INTERVAL= + +BRIG_CASSANDRA_HOST= +BRIG_CASSANDRA_PORT= +BRIG_CASSANDRA_KEYSPACE= +``` + +1. Create the new index + ```bash + docker run "quay.io/wire/brig-index:$WIRE_VERSION" create \ + --elasticsearch-server "http://$ES_NEW_HOST:$ES_NEW_PORT" \ + --elasticsearch-index "$ES_NEW_INDEX" \ + --elasticsearch-shards "$SHARDS" \ + --elasticsearch-replicas "$REPLICAS" \ + --elasticsearch-refresh-interval "$REFRESH_INTERVAL" + ``` +1. Redeploy brig with `elasticsearch.additionalWriteIndexUrl` set to the URL of + the new cluster and `elasticsearch.additionalWriteIndex` set to + `$ES_NEW_INDEX`. +1. Make sure no old instances of brig are running. +1. Reindex data to the new index + ```bash + docker run "quay.io/wire/brig-index:$WIRE_VERSION" migrate-data \ + --elasticsearch-server "http://$ES_NEW_HOST:$ES_NEW_PORT" \ + --elasticsearch-index "$ES_NEW_INDEX" \ + --cassandra-host "$BRIG_CASSANDRA_HOST" \ + --cassandra-port "$BRIG_CASSANDRA_PORT" \ + --cassandra-keyspace "$BRIG_CASSANDRA_KEYSPACE" + --galley-host "$GALLEY_HOST" + --galley-port "$GALLEY_PORT" + ``` +1. Remove `elasticsearch.additionalWriteIndex` and + `elasticsearch.additionalWriteIndexUrl` from brig config. Set + `elasticsearch.url` to the URL of the new cluster and `elasticsearch.index` + to the name of new index. Deploy brig with these settings. + +## Recreate an index (Requires downtime) + +When analysis settings of an index need to be changed, e.g. for changes +introduced in [#1052](https://github.com/wireapp/wire-server/pull/1052), +it is not possible to keep the index running while the changes are applied. + +To tackle this, a wire-server operator must either migrate to a new index as +documented [above](#migrate-to-a-new-index) or allow for some downtime. One +might want to choose downtime for simplicity. These steps are especially simple +to do when using [wire-server-deploy](https://github.com/wireapp/wire-server-deploy/). + +Here are the steps: + +Before starting, please set these environment variables +```bash +ES_HOST= +ES_PORT= # default is 9200 +ES_INDEX= +``` + +### Step 1: Delete the old index +```bash +curl -XDELETE http://$ES_HOST:$ES_PORT/$ES_INDEX +curl -XDELETE http://$ES_HOST:$ES_PORT/wire_brig_migrations +``` + +### Step 2: Recreate the index + +#### When using helm charts from [wire-server-deploy](https://github.com/wireapp/wire-server-deploy) + +Just redeploy the helm chart, new index will be created and after the deployment +data migrations will refill the index with users. + + +#### When not using helm charts from [wire-server-deploy](https://github.com/wireapp/wire-server-deploy) + +Set these extra environment variables: +```bash +WIRE_VERSION= +SHARDS= +REPLICAS= +REFRESH_INTERVAL= +``` +1. Create the index + ```bash + docker run "quay.io/wire/brig-index:$WIRE_VERSION" create \ + --elasticsearch-server "http://$ES_HOST:$ES_PORT" \ + --elasticsearch-index "$ES_INDEX" \ + --elastcsearch-shards "$SHARDS" \ + --elastcsearch-replicas "$REPLICAS" \ + --elastcsearch-refresh-interval "$REFRESH_INTERVAL" + ``` +1. Refill the index as documented [above](#refill-es-documents-from-cassandra) diff --git a/docs/legacy/reference/elasticsearch-migration-2021-02-16.md b/docs/legacy/reference/elasticsearch-migration-2021-02-16.md new file mode 100644 index 00000000000..e96c464acb9 --- /dev/null +++ b/docs/legacy/reference/elasticsearch-migration-2021-02-16.md @@ -0,0 +1,24 @@ +# ElasticSearch migration instructions for release 2021-02-16 + +Release `2021-02-16` of `wire-server` requires an update of the ElasticSearch index of `brig`. +During the update the team member search in TeamSettings will be defunct. + +The update is triggered automatically on upgrade by the `elasticsearch-index-create` and `brig-index-migrate-data` jobs. If these jobs finish sucessfully the update is complete. + +## Troubleshooting + +In case the `elasticsearch-index-create` job fails this document describes how to create a new index. + +The index that brig is using is defined at `brig.config.elasticsearch.index` of the `wire-server` chart. We will refer to its current setting as ``. + +1. Choose a new index name that is different from ``. + We will refer to this name as ``. +2. Upgrade the release with these config changes: + - Set `brig.config.elasticsearch.additionalWriteIndex` to `` + - Set `elasticsearch-index.elasticsearch.additionalWriteIndex` to `` + and wait for completion. +3. Upgrade the release again with these config changes: + - Unset `brig.config.elasticsearch.additionalWriteIndex` + - Unset `elasticsearch-index.elasticsearch.additionalWriteIndex` + - Set `brig.config.elasticsearch.index` to `` + - Set `elasticsearch-index.elasticsearch.index` to `` diff --git a/docs/legacy/reference/make-docker-and-qemu.md b/docs/legacy/reference/make-docker-and-qemu.md new file mode 100644 index 00000000000..3088eda1a0d --- /dev/null +++ b/docs/legacy/reference/make-docker-and-qemu.md @@ -0,0 +1,1072 @@ +# About this document: +This document is written with the goal of explaining https://github.com/wireapp/wire-server/pull/622 well enough that someone can honestly review it. :) + +In this document, we're going to rapidly bounce back and forth between GNU make, bash, GNU sed, Docker, and QEMU. + +# What does this Makefile do? Why was it created? + +To answer that, we're going to have to go back to Wire-Server, specifically, our integration tests. Integration tests are run locally on all of our machines, in order to ensure that changes we make to the Wire backend do not break currently existing functionality. In order to simulate the components that wire's backend depends on (s3, cassandra, redis, etc..), we use a series of docker images. These docker images are downloaded from dockerhub, are maintained (or not maintained) by outside parties, and are built by those parties. + +When a docker image is built, even if the docker image is something like a java app, or a pile of perl/node/etc, the interpreters (openjdk, node, perl) are embedded into the image. Those interpreters are compiled for a specific processor architecture, and only run on that architecture (and supersets of it). For instance, an AMD64 image will run on only an AMD64 system, but a 386 image will run on AMD64 since AMD64 is a superset of 386. Neither of those images will run on an ARM, like a Raspberry pi. + +This Makefile contains rules that allow our Mac users to build all of the docker images locally on their machine, with some minor improvements, which will save us about 2.5G of ram during integration tests. Additionally, it contains rules for uploading these images to dockerhub for others to use, and support for linux users to build images for arm32v5, arm32v7, 386, and AMD64, despite not being on these architectures. + +It builds non-AMD64 images on linux by using QEMU, a system emulator, to allow docker to run images that are not built for the architecture the system is currently running on. This is full system emulation, like many video game engines you're probably familiar with. You know how you have to throw gobs of hardware at a machine, to play a game written for a gaming system 20 years ago? This is similarly slow. To work around this, the Makefile is written in a manner that allows us to build many docker images at once, to take advantage of the fact that most of us have many processor cores lying around doing not-all-much. + +# What does this get us? + +To start with, the resulting docker images allow us to tune the JVM settings on cassandra and elasticsearch, resulting in lower memory consumption, and faster integration tests that don't impact our systems as much. Additionally, it allows us more control of the docker images we're depending on, so that another leftpad incident on docker doesn't impact us. As things stand, any of the developers of these docker images can upload a new docker image that does Very Bad Things(tm), and we'll gladly download and run it many times a day. Building these images ourselves from known good GIT revisions prevents this. Additionally, the multi-architecture approach allows us to be one step closer to running the backend on more esoteric systems, like a Raspberry pi, or an AWS A instance, both of which are built on the ARM architecture. Or, if rumour is to be believed, the next release of MacBook Pros. :) + +# Breaking it down: + +## Docker: + +to start with, we're going to have to get a bit into some docker architecture. We all have used docker, and pretty much understand the following workflow: + +I build a docker image from a Dockerfile and maybe some additions, I upload it to dockerhub, and other people can download and use the image. I can use the locally built image directly, without downloading it from dockerhub, and I can share the Dockerfile and additions via git, on github, and allow others to build the image. + +While this workflow works well for working with a single architecture, we're going to have to introduce some new concepts in order to support the multiple architecture way of building docker files. + +### Manifest files. + +Manifest files are agenerated by docker and contain references to multiple docker images, one for each architecture a given docker image has been built for. Each image in the manifest file is tagged with the architecture that the image is built for. + +Docker contains just enough built-in logic to interpret a manifest file on dockerhub, and download an image that matches the architecture that docker was built for. When using a manifest file, this is how docker determines what image to download. + +### A Manifest centric Workflow: + +If you're building a docker image for multiple architectures, you want a Manifest, so that docker automatically grabs the right image for the user's machine. This changes our workflow from earlier quite a bit: + +I build a docker image from a Dockerfile, and I build other images from slightly different versions of this Dockerfile (more on this later). I tag these images with a suffix, so that I can tell them apart. I upload the images to dockerhub, retaining the tags that differentiate the diffenent versions from each other. I create a manifest file, referring to the images that have been pushed to DockerHub, and upload the manifest file to DockerHub. People can download and use the image from dockerhub by refering to the tag of the manifest file. I can share the Dockerfile and additions via git, on dockerhub, and others can build their own images from it. + +#### What does this look like? + +All of us on the team are using AMD64 based machines, so in this example, we're going to build one image for AMD64, and one for it's predecessor architecture, I386. We're going to build the SMTP server image we depend on, from https://hub.docker.com/r/namshi/smtp. We're going to use a known safe git revision, and use some minor GNU sed to generate architecture dependent Dockerfiles from the Dockerfile in git. Everyone should be able to do this on your laptops. + +```bash +$ git clone https://github.com/namshi/docker-smtp.git smtp +Cloning into 'smtp'... +remote: Enumerating objects: 4, done. +remote: Counting objects: 100% (4/4), done. +remote: Compressing objects: 100% (4/4), done. +remote: Total 126 (delta 0), reused 0 (delta 0), pack-reused 122 +Receiving objects: 100% (126/126), 26.57 KiB | 269.00 KiB/s, done. +Resolving deltas: 100% (61/61), done. +$ cd smtp +$ git reset --hard 8ad8b849855be2cb6a11d97d332d27ba3e47483f +HEAD is now at 8ad8b84 Merge pull request #48 from zzzsochi/master +$ cat Dockerfile | sed "s/^\(MAINTAINER\).*/\1 Julia Longtin \"julia.longtin@wire.com\"/" | sed "s=^\(FROM \)\(.*\)$=\1i386/\2=" > Dockerfile-386 +$ cat Dockerfile | sed "s/^\(MAINTAINER\).*/\1 Julia Longtin \"julia.longtin@wire.com\"/" | sed "s=^\(FROM \)\(.*\)$=\1\2=" > Dockerfile-amd64 +$ docker build -t julialongtin/smtp:0.0.9-amd64 -f Dockerfile-amd64 + +$ docker build -t julialongtin/smtp:0.0.9-386 -f Dockerfile-386 + +$ docker push julialongtin/smtp:0.0.9-amd64 + +$ docker push julialongtin/smtp:0.0.9-386 +] 271.46K --.-KB/s in 0.07s + +2019-03-06 14:27:39 (3.65 MB/s) - ‘sash_3.8-5_armel.deb’ saved [277976/277976] +$ +``` + +This deb will not install on our machine, so we're going to manually take it apart, to get the sash binary out of it. + +```bash +$ mkdir tmp +$ cd tmp +$ ar x ../sash_3.8-5_armel.deb +$ ls + control.tar.xz data.tar.xz +$ tar -xf data.tar.gz +$ ls -la bin/sash +-rwxr-xr-x 1 demo demo 685348 Jun 9 2018 bin/sash +``` + +to verify what architecture this binary is built for, use the 'file' command. +```bash +$ file bin/sash +bin/sash: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=20641a8ca21b2c320ea7e6079ec88b857c7cbcfb, stripped +$ +``` + +now we can run this, and even run Arm64 programs that are on our own machine using it. +```bash +$ bin/sash +Stand-alone shell (version 3.8) +> file bin/sash +bin/sash: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=20641a8ca21b2c320ea7e6079ec88b857c7cbcfb, stripped +> ls +bin usr +> uname -a +Linux boxtop 4.9.0-8-amd64 #1 SMP Debian 4.9.144-3 (2019-02-02) x86_64 GNU/Linux +> whoami +demo +> +``` + +## QEMU, BinFmt, and Docker (Oh my!) + +After following the directions in the last two sections, you've created two docker images (one for i386, one for AMD64), created a manifest referring to them, set up for linux to load qemu and use it, and launched a binary for another architecture. + +Creating non-native docker images can now be done very similar to how i386 was done earlier. + +Because you are using a system emulator, your docker builds for non-x86 will be slower. additionally, the emulators are not perfect, so some images won't build. finally, code is just less tested on machines that are not an AMD64 machine, so there are generally more bugs. + +### Arm Complications: +The 32 bit version of arm is actually divided into versions, and not all linux distributions are available for all versions. arm32v5 and arm32v7 are supported by debian, while arm32v6 is supported by alpine. This variant must be specified during manifest construction, so to continue with our current example, these are the commands for tagging the docker images for our arm32v5 and arm32v7 builds of smtp: +```bash +$ docker manifest annotate julialongtin/smtp:0.0.9 julialongtin/smtp:0.0.9-arm32v5 --arch arm --variant 5 +$ docker manifest annotate julialongtin/smtp:0.0.9 julialongtin/smtp:0.0.9-arm32v7 --arch arm --variant 7 +``` + + +# Into the GNU Make Abyss + +Now that we've done all of the above, we should be capable of working with docker images independent of the architecture we're targeting. Now, into the rabit hole we go, automating everything with GNU Make + +## Why Make? +GNU make is designed to build targets by looking at the environment it's in, and executing a number of rules depending on what it sees, and what it has been requested to do. The Makefile we're going to look through does all of the above, along with making some minor changes to the docker images. It does this in parallel, calling as many of the commands at once as possible, in order to take advantage of idle cores. + +## Using the Makefile + +Before we take the Makefile apart, let's go over using it. + +This Makefile is meant to be used in four ways: building a set of images, pushing (and building) a set of images, building a single image. It follows the manifest workflow we documented earlier. + +By default, running 'make' in the same directory as the Makefile (assuming you've set all of the above up correctly) will attempt to build and push all of the docker images the makefile knows about to dockerhub. If you want this to work, you need to create a dockerhub account, use 'docker login' to log your local instance of docker in to dockerhub, then you need to create a repository for each docker image. + +To get a list of the names of the docker images this Makefile knows about, run 'make names'. +```bash +$ make names +Debian based images: +airdock_fakesqs airdock_rvm airdock_base smtp dynamodb_local cassandra +Alpine based images: +elasticsearch java_maven_node_python localstack minio +$ +``` + +The list of names is divided into two groups. one group is for images based on debian, and the other is for images based on alpine. This makefile can only build for one of these two distributions at once. + +Since no-one wants to click through dockerhub to create repositories, let's just build docker images locally, for now. + +Make looks at it's environment in order to decide what to do, so here are some environment variables that we're going to use. all of these variables have default values, so we're only going to provide a few of them. + +- `ARCHES`: the list of architectures we're going to attempt docker builds for. Mac users should supply "386 AMD64" to this, as they have no binfmt support. +- `DIST`: the distribution we're going to build for. this can be either DEBIAN or ALPINE. +- `DOCKER`_USERNAME: our username on dockerhub. +- `DOCKER`_EMAIL: Our email address, as far as dockerhub is concerned. +- `DOCKER`_REALNAME: again, our name string that will be displayed in DockerHub. +- `SED`: which sed binary to use. Mac users should install GSED, and pass the path to it in this variable. + +To build all of the debian based images locally on my machine, I run +```bash +make DIST=DEBIAN DOCKER_USERNAME=julialongtin DOCKER_EMAIL=julia.longtin@wire.com DOCKER_REALNAME='Julia Longtin' build-all -j". +``` + +What's the -j for? adding a '-j' to the command line causes make to execute in parallel. That's to say, it will try to build ALL of the images at once, taking care to build images that are dependencies of other images before building the images that depend on them. + +Note that since we are building the images without pushing them to DockerHub, no manifest files are generated. + +If we want to use these images in our docker compose, we can edit the docker compose file, and refer to the image we want with it's architecture suffix attached. This will make docker-compose use the local copy, instead of hitting DockerHub, grabbing the manifest, and using an image from there. for instance, to use the local cassandra image I just built, I would edit the docker-compose.yaml file in our wire-server repo, and make the cassandra section look like the following: + +``` + cassandra: + container_name: demo_wire_cassandra + #image: cassandra:3.11.2 + image: julialongtin/cassandra:0.0.9-amd64 + ports: + - "127.0.0.1:9042:9042" + environment: +# what's present in the jvm.options file by default. +# - "CS_JAVA_OPTIONS=-Xmx1024M -Xms1024M -Xmn200M" + - "CS_JVM_OPTIONS=-Xmx128M -Xms128M -Xmn50M" + networks: + - demo_wire +``` + +To remove all of the git repositories containing the Dockerfiles we download to build these images, we can run `make clean`. There is also the option to run `make cleandocker` to REMOVE ALL OF THE DOCKER IMAGES ON YOUR MACHINE. careful with that one. Note that docker makes good use of caching, so running 'make clean' and the same make command you used to build the images will complete really fast, as docker does not actually need to rebuild the images. + +## Reading through the Makefile + +OK, now that we have a handle on what it does, and how to use it, let's get into the Makefile itsself. + +A Makefile is a series of rules for performing tasks, variables used when creating those tasks, and some minimal functions and conditional structures. Rules are implemented as groups of bash commands, where each line is handled by a new bash interpreter. Personally, I think it 'feels functiony', only without a type system and with lots of side effects. Like if bash tried to be functional. + +### Variables + +#### Overrideable Variables +the make language has multiple types of variables and variable assignments. To begin with, let's look at the variables we used in the last step. +```bash +$ cat Makefile | grep "?=" +DOCKER_USERNAME ?= wireserver +DOCKER_REALNAME ?= Wire +DOCKER_EMAIL ?= backend@wire.com +TAGNAME ?= :0.0.9 +DIST ?= DEBIAN +LOCALARCH ?= $(call dockerarch,$(LOCALDEBARCH)) + ARCHES ?= $(DEBARCHES) + NAMES ?= $(DEBNAMES) + ARCHES ?= $(ALPINEARCHES) + NAMES ?= $(ALPINENAMES) +SED ?= sed +SMTP_COMMIT ?= 8ad8b849855be2cb6a11d97d332d27ba3e47483f +DYNAMODB_COMMIT ?= c1eabc28e6d08c91672ff3f1973791bca2e08918 +ELASTICSEARCH_COMMIT ?= 06779bd8db7ab81d6706c8ede9981d815e143ea3 +AIRDOCKBASE_COMMIT ?= 692625c9da3639129361dc6ec4eacf73f444e98d +AIRDOCKRVM_COMMIT ?= cdc506d68b92fa4ffcc7c32a1fc7560c838b1da9 +AIRDOCKFAKESQS_COMMIT ?= 9547ca5e5b6d7c1b79af53e541f8940df09a495d +JAVAMAVENNODEPYTHON_COMMIT ?= 645af21162fffd736c93ab0047ae736dc6881959 +LOCALSTACK_COMMIT ?= 645af21162fffd736c93ab0047ae736dc6881959 +MINIO_COMMIT ?= 118270d76fc90f1e54cd9510cee9688bd717250b +CASSANDRA_COMMIT ?= 064fb4e2682bf9c1909e4cb27225fa74862c9086 +``` + +The '?=' assignment operator is used to provide a default value. When earlier, we ran make as "make DIST=DEBIAN DOCKER_USERNAME=julialongtin DOCKER_EMAIL=julia.longtin@wire.com DOCKER_REALNAME='Julia Longtin' build-all -j", we were overriding those values. the Make interpreter will use values provided on the command line, or values we have used 'export' to place into our shell environment. + +LOCALARCH and the assignments for ARCHES and NAMES are a bit different. LOCALARCH is a function call, and the ARCHES and NAMES are emdedded in conditional statements. We'll cover those later. + +Note the block of COMMIT IDs. This is in case we want to experiment with newer releases of each of the docker images we're using. Fixing what we're using to a commit ID makes it much harder for an upstream source to send us malicious code. + +#### Non-Overrideable Variables +The following group of variables use a different assignment operator, that tells make not to look in the environment first. +```bash +$ cat Makefile | grep ":=" +USERNAME := $(DOCKER_USERNAME) +REALNAME := $(DOCKER_REALNAME) +EMAIL := $(DOCKER_EMAIL) +STRETCHARCHES := arm32v5 arm32v7 386 amd64 arm64v8 ppc64le s390x +JESSIEARCHES := arm32v5 arm32v7 386 amd64 +DEBARCHES := arm32v5 arm32v7 386 amd64 +JESSIENAMES := airdock_fakesqs airdock_rvm airdock_base smtp +STRETCHNAMES := dynamodb_local cassandra +DEBNAMES := $(JESSIENAMES) $(STRETCHNAMES) +ALPINEARCHES := amd64 386 arm32v6 +ALPINENAMES := elasticsearch java_maven_node_python localstack minio +PREBUILDS := airdock_rvm-airdock_base airdock_fakesqs-airdock_rvm localstack-java_maven_node_python +NOMANIFEST := airdock_rvm airdock_fakesqs localstack +LOCALDEBARCH := $(shell [ ! -z `which dpkg` ] && dpkg --print-architecture) +BADARCHSIM := localstack-arm32v6 java_maven_node_python-arm32v6 dynamodb_local-386 +$ +``` + +The first three variable assignments are referring to other variables. These basically exist as alias, to make our make rules denser later. + +STRETCHARCHES and JESSIEARCHES contain the list of architectures that dockerhub's debian stretch and jessie images provide. DEBARCHES defines what architectures we're going to build, for our debian targets. STRETCHARCHES and DEBIANARCHES only exist to make it visible to readers of the Makefile which images CAN be built for which architectures. + +JESSIENAMES and STRETCHNAMES are used similarly, only they are actually referred to by DEBNAMES, to provide the list of debian based images that can be built. + +ALPINEARCHES and ALPINENAMES work similarly, and are used when we've provided "DIST=ALPINE". We do not divide into seperate variables quite the same way as debian, because all of our alpine images are based on alpine 3.7. + +PREBUILDS contains our dependency map. essentially, this is a set of pairs of image names, where the first image mentioned depends on the second image. so, airdock_rvm depends on airdock_base, where airdock_fakesqs depends on airdock_rvm, etc. this means that our docker image names may not contain `-`s. Dockerhub allows it, but this makefile needed a seperator... and that's the one I picked. + +BADARCH is similar, pairing the name of an image with the architecture it fails to build on. This is so I can blacklist things that don't work yet. + +LOCALDEBARCH is a variable set by executing a small snippet of bash. The snippet makes sure dpkg is installed (the debian package manager), and uses dpkg to determine what the architecture of your local machine is. As you remember from when we were building docker images by hand, docker will automatically fetch an image that is compiled for your current architecture, so we use LOCALDEBARCH later to decide what architectures we need to fetch with a prefix or postfix, and which we can fetch normally. + +NOMANIFEST lists images that need a work-around for fetching image dependencies for specific architectures. You know how we added the name of the architecture BEFORE the image name in the dockerfiles? well, in the case of the dependencies of the images listed here, dockerhub isn't supporting that. DockerHub is supporting that form only for 'official' docker images, like alpine, debian, etc. as a result, in order to fetch an architecture specific version of the dependencies of these images, we need to add a - suffix. like -386 -arm32v7, etc. + +### Conditionals +We don't make much use of conditionals, but there are three total uses in this Makefile. let's take a look at them. + +In order to look at our conditionals (and many other sections of this Makefile later), we're going to abuse sed. If you're not comfortable with the sed shown here, or are having problems getting it to work, you can instead just open the Makefile in your favorite text editor, and search around. I abuse sed here for both brevity, and to encourage the reader to understand complicated sed commands, for when we are using them later IN the Makefile. + +SED ABUSE: +to get our list of conditionals out of the Makefile, we're going to use some multiline sed. specifically, we're going to look for a line starting with 'ifeq', lines starting with two spaces, then the line following. + +```bash +$ cat Makefile | sed -n '/ifeq/{:n;N;s/\n /\n /;tn;p}' +ifeq ($(LOCALARCH),) + $(error LOCALARCH is empty, you may need to supply it.) + endif +ifeq ($(DIST),DEBIAN) + ARCHES ?= $(DEBARCHES) + NAMES ?= $(DEBNAMES) +endif +ifeq ($(DIST),ALPINE) + ARCHES ?= $(ALPINEARCHES) + NAMES ?= $(ALPINENAMES) +endif +$ +``` + +There's a lot to unpack there, so let's start with the simple part, the conditionals. +The conditionals are checking for equality, in all cases. +First, we check to see if LOCALARCH is empty. This can happen if dpkg was unavailable, and the user did not supply a value on the make command line or in the user's bash environment. if that happens, we use make's built in error function to display an error, and break out of the Makefile. +The second and third conditionals decide on the values of ARCHES and NAMES. Earlier, we determined the default selection for DIST was DEBIAN, so this pair just allows the user to select ALPINE instead. note that the variable assignments in the conditionals are using the overrideable form, so the end user can override these on make's command line or in the user's environment. mac users will want to do this, since they don't have QEMU available in the same form, and are limited to building X86 and AMD64 architecture. + +Note that conditionals are evaluated when the file is read, once. This means that we don't have the ability to use them in our rules, or in our functions, and have to abuse other operations in 'functionalish' manners... + +Now, back to our sed abuse. +SED is a stream editor, and quite a powerful one. In this case, we're using it for a multi-line search. we're supplying the -n option, which squashes all output, except what sed is told specificly to print something with a command. +Let's look at each of the commands in that statement seperately. +```sed +# find a line that has 'ifeq' in it. +/ifeq/ +# begin a block of commands. every command in the block should be seperated by a semicolon. +{ +# create an anchor, that is to say, a point that can be branched to. +:n; +# Append the next line into the parameter space. so now, for the first block, the hold parameter space would include "ifeq ($(LOCALARCH),)\n $(error LOCALARCH is empty, you may need to supply it.)". +N; +# Replace the two spaces in the parameter space with one space. +s/\n /\n /; +# If the previous 's' command found something, and changed something, go to our label. +tn; +# print the contents of the parameter space. +p +# close the block of commands. +} +``` +... Simple, right? + +note that the contents above can be stored to a file, and run with sed's "-f" command, for more complicated sed scripts. Sed is turing complete, so... things like tetris have been written in it. My longest sed scripts do things like sanity check OS install procedures, or change binaryish protocols into xmlish forms. + +### Functions +Make has a concept of functions, and the first two functions we use are a bit haskell inspired. + +SED ABUSE: +To get a list of the functions in our makefile, we're going to use a bit more traditional sed. specifically, we're going to look for lines that start with a number of lowercase characters that are immediately followed by an '=' sign. + +```bash +$ cat Makefile | sed -n '/^[a-z]*=/p' +dockerarch=$(patsubst i%,%,$(patsubst armel,arm32v5,$(patsubst armhf,arm32v7,$(patsubst arm64,arm64v8,$(1))))) +fst=$(word 1, $(subst -, ,$(1))) +snd=$(word 2, $(subst -, ,$(1))) +goodarches=$(filter-out $(call snd,$(foreach arch,$(ARCHES),$(filter $(1)-$(arch),$(BADARCHSIM)))),$(ARCHES)) +nodeps=$(filter-out $(foreach target,$(NAMES),$(call snd,$(foreach dependency,$(NAMES),$(filter $(target)-$(dependency),$(PREBUILDS))))),$(NAMES)) +maniarch=$(patsubst %32,%,$(call fst,$(subst v, ,$(1)))) +manivariant=$(foreach variant,$(word 2, $(subst v, ,$(1))), --variant $(variant)) +archpostfix=$(foreach arch,$(filter-out $(filter-out $(word 3, $(subst -, ,$(filter $(call snd,$(1))-%-$(call fst,$(1)),$(foreach prebuild,$(PREBUILDS),$(prebuild)-$(call fst,$(1)))))),$(LOCALARCH)),$(call fst,$(1))),-$(arch)) +archpath=$(foreach arch,$(patsubst 386,i386,$(filter-out $(LOCALARCH),$(1))),$(arch)/) +$ +``` + +These are going to be a bit hard to explain in order, especially since we haven't covered where they are being called from. Let's take them from simplest to hardest, which happens to co-incide with shortest, to longest. + +The fst and snd functions are what happens when a haskell programmer is writing make. You remember all of the pairs of values earlier, that were seperated by a single '-' character? these functions return either the first, or the second item in the pair. Let's unpack 'fst'. +fst uses the 'word' function of make to retrieve the first word from "$(subst -, ,$(1))". the 'subst' function substitutes a single dash for a single space. this seperates a - pair into a space seperated string. $(1) is the first argument passed to this function. +snd works similarly, retrieving from our pair. + +The next easiest to explain function is 'maniarch'. It returns the architecture string that we use when annotating a docker image. When we refer to an architecture, we use a string like 'amd64' or 'arm32v6', but docker manifest wants just 'arm' 'amd64' or '386'. +maniarch first uses the 'patsubst' command to replace "anystring32" with "anystring". this removes the 32 from arm32. It's given the result of $(call fst,$(subst v, ,$(1)))) as a string to work with. +$(call fst,$(subst v, ,$(1)))) calls our 'fst' function, giving it the result of us substituting 'v' for ' ' in the passed in argument. in the case of arm32v6, it seperates the string into "arm32 6". Note that instead of calling fst, we could have just used 'word 1' like we did in fst. This is a mistake on my part, but it works regardless, because of the way fst is built. as before, $(1) is the argument passed into our function. + +manivariant has a similar function to maniarch. It's job is to take an architecture name (amd64, arm32v5, etc...), and if it has a 'v', to return the '--variant ' command line option for our 'docker manifest anotate'. +manivariant starts by using make's 'foreach' function. this works by breaking it's second argument into words, storing them into the variable name given in the first argument, and then generating text using the third option. this is a bit abusive, as we're really just using it as "if there is a variant, add --variant " structure. +The first argument of foreach is the name of a variable. we used 'variant' here. the second argument in this case properly uses word, and subst to return only the content after a 'v' in our passed in argument, or emptystring. the third option is ' --variant $(variant)', using the variable defined in the first parameter of foreach to create " --variant 5" if this is passed "arm32v5", for instance. + +archpath is similar in structure to manivariant. In order to find a version of a docker image that is appropriate for our non-native architectures, we have to add the 'archname/' string to the path to the image we're deriving from, in our Dockerfile. This function returns that string. We start by using foreach in a similar method as manivariant, to only return a string if the second argument to foreach evaluates to content. In our second argument, we begin by performing a patsubst, replacing a '386' with an 'i386' if it's found in the patsubst argument. This is because on dockerhub, official images of different architectures are actually stored in a series of machine maintained accounts, and an account name can't start with a number. therefore, 386 images are stored under a user called 'i386'. As an argument to the patsubst, we're providing our first usage of filter-out. it's used here so that if the local architecture was supplied to this function, the string will return empty in section 2 of our foreach, and therefore the third section won't even be evaluated. + +our next function to explain is 'goodarches'. This function is passed an image name, and returns all of the arches from our architecture list that that image can be built for. It basically searches BADARCHSIM from earlier, and removes an architecture from the returned copy of ARCHES if a - pair for that architecture exists. We use filter-out to remove anything returned from it's second argument from the ARCHES list we provide as it's third argument. The second argument to filter-out uses snd to seperate the architecture name from a string found, and uses foreach and filter to search BADARCHSIM for all possible combinations between the passed in image name, and all of the architectures. + +dockerarch is a bit simpler than the last few. it takes the debian architecture name, replacing it with the docker architecture name, using a series of nested patsubst substititions. + +Unlike our earlier functions, nodeps does not require an argument. It's function is to return a list of images from NAMES that do not have any dependencies. To do this, we start with a filter-out of NAMES, then use a pair of nested foreach statements, both searching through NAMES, and constructing all combinations of -. This value is looked for in PREBUILDS, and if a combination is found, we use snd to return the dependency to filter-out. this is probably overly complicated, and can likely be shortened by the use of patsubst. "it works, ship it." + +Finally, we get to archpostfix. archpostfix has a similar function to archpath, only it provides a - for the end of the image path if the DEPENDENCY of this image is not an official image, and therefore can not be found by adding an 'arch/' postfix. This is long, and probably also a candidate for shortening. Reading your way through this one is an exercise for when the reader wants a reverse polish notation headache. + + +To summarize the Make functions we've (ab)used in this section: +``` +$(word 1,string of words) # return the Nth word in a space separated string. +$(subst -, ,string-of-words) # replace occurances of '-' with ' ' in string. +$(patsubst string%,%string) # replace a patern with another patern, using % as a single wildcard. +$(call function,argument) # function calling. +$(foreach var,string,$(var)) # iterate on a space seperated string, evaluating the last argument with var set to each word in string. +$(filter word,word list) # return word if it is found in word list. +$(filter-out word, word list) # return word list without word. +``` + +Now after all of that, let's go through the SED command we last used. Remember that? +```bash +$ cat Makefile | sed -n '/^[a-z]*=/p' +``` +Again, we're going to use sed in '-n' mode, supressing all output except the output we are searching for. /PATTERN/ searches the lines of the input for a pattern, and if it's found, the command afterward is executed, which is a 'p' for print, in this case. the patern given is '^[a-z]*='. The '^' at the beginning means 'look for this patern at the beginning of the line, and the '=' at the end is the equal sign we were looking for. '[a-z]*' is us using a character class. character classes are sedspeak for sets of characters. they can be individually listed, or in this case, be a character range. the '*' after the character class just means "look for these characters any number of times". technically, that means a line starting in '=' would work (since zero is any number of times), but luckily, our file doesn't contain lines starting with =, as this is not valid make syntax. + +### Rules. + +Traditionally, makefiles are pretty simple. they are used to build a piece of software on your local machine, so you don't have to memorize all of the steps, and can type 'make', and have it just done. A simple Makefile looks like the following: +```make +CC=gcc +CFLAGS=-I. +DEPS = hellomake.h + +%.o: %.c $(DEPS) + $(CC) -c -o $@ $< $(CFLAGS) + +hellomake: hellomake.o hellofunc.o + $(CC) -o hellomake hellomake.o hellofunc.o + +clean: + rm hellomake hellomake.o hellofunc.o +``` +This example Makefile has some variables, and rules, that are used to build a C program into an executable, using GCC. + +Our Makefile is much more advanced, necessatating this document, to ensure maintainability. + + +A single make rule is divided into three sections: what you want to build, what you need to build first, and the commands you run to build the thing in question: +```make +my_thing: things I need first + bash commands to build it + +target: prerequisites + recipe line 1 +``` + +The commands to build a thing (recipe lines) are prefaced with a tab character, and not spaces. Each line is executed in a seperate shell instance. + + +#### The roots of the trees + +In the section where we showed you how to use our Makefile, we were calling 'make' with an option, such as push-all, build-smtp, names, or clean. We're now going to show you the rules that implement these options. + +SED ABUSE: +This time, we're going to add the -E command to sed. this kicks sed into the 'extended regex' mode, meaning for our purposes, we don't have to put a \ before a ( or a ) in our regex. we're then going to use a patern grouping, to specify that we want either the clean or names rules. we're also going to swap the tabs for spaces, to prevent our substitution command from always matching, and not even visibly change the output. total cheating. +```bash +$ cat Makefile | sed -n -E '/^(clean|names)/{:n;N;s/\n\t/\n /;tn;p}' +clean: + rm -rf elasticsearch-all airdock_base-all airdock_rvm-all airdock_fakesqs-all cassandra-all $(DEBNAMES) $(ALPINENAMES) + +cleandocker: + docker rm $$(docker ps -a -q) || true + docker rmi $$(docker images -q) --force || true + +names: + @echo Debian based images: + @echo $(DEBNAMES) + @echo Alpine based images: + @echo $(ALPINENAMES) +``` + +Most Makefiles change their environment. Having changed their environment, most users want a quick way to set the environment back to default, so they can make changes, and build again. to enable this, as a convention, most Makefiles have a 'clean' rule. Ours remove the git repos that we build the docker images from. note the hardcoded list of '-all' directories: these are the git repos for images where the git repo does not simply have a Dockerfile at the root of the repo. In those cases, our rules that check out the repos check them out to -all, then do Things(tm) to create a /Dockerfile. + +cleandocker is a rule I use on my machine, when docker images have gotten out of control. it removes all of the docker images on my machine, and is not meant to be regularly run. + +names displays the names of the images this Makefile knows about. It uses a single @ symbol at the beginning of the rules. this tells make that it should NOT display the command that make is running, when make runs it. + +OK, that covers the simple make rules, that have no dependencies, or parameters. Now let's take a look at our build and push rules. these are the 'top' of a dependency tree, which is to say they depend on things, that depend on things... that do the think we've asked for. + +```bash +$ cat Makefile | sed -n -E '/^(build|push|all)/{:n;N;s/\n\t/\n /;tn;p}' +all: $(foreach image,$(nodeps),manifest-push-$(image)) + +build-%: $$(foreach arch,$$(call goodarches,%),create-$$(arch)-$$*) + @echo -n + +build-all: $(foreach image,$(nodeps),build-$(image)) + +push-%: manifest-push-% + @echo -n + +push-all: $(foreach image,$(nodeps),manifest-push-$(image)) +$ +``` + +Lets take these simplest to most complex. + +push-% is the rule called when we run 'make push-'. It depends on manifest-push-%, meaning that make will take whatever you've placed after the 'push-', look for a rule called manifest-push-, and make sure that rule completes, before trying to execute this rule. Executing this rule just executes nothing, and in reality, the '@echo -n" exists to allow the push-% rule to be executed. By default, make considers wildcard rules as phony, meaning they cannot be called from the command line, and must be called from a rule with no wildcarding. + +push-all is allowed to have no commands, because it's name contains no wildcard operator. In it's dependency list, we're using a foreach loop to go through our list of images that have no dependencies, and ask for manifest-push- to be built. + +all is identical to push-all. I could have just depended on push-all, and saved some characters here. + +build-all operates similar to push-all, only it asks for build- to be built for all of the no-dependency images. + +build-% combines the approach of push-% and build-all. It uses foreach to request the build of create--, which builds one docker image for each architecture that we know this image will build on. This is our first exposure to $$ structures, so let's look at those a bit. + +By default, make allows for one % in the build-target, and one % in the dependencies. it takes what it matches the % against in the build-target, and substitutes the first % found in the dependency list with that content. so, what do you do if you need to have the thing that was matched twice in the dependency list? enter .SECONDEXPANSION. + +```bash +$ cat Makefile | sed -n -E '/^(.SECOND)/{:n;N;s/\n\t/\n /;tn;p}' | less +.SECONDEXPANSION: + +``` + +.SECONDEXPANSION looks like a rule, but really, it's a flag to make, indicating that dependency lists in this Makefile should be expanded twice. During the first expansion, things will proceed as normal, and everything with two dollar signs will be ignored. during the second expansion things that were delayed by using two dollar signs are run, AND a set of variables that is normally available in the 'recipe' section. In the case we're looking at, this means that during the first expansion, only the "%" character will be interpreted. during the second expansion the foreach and call will actually be executed, and the $$* will be expanded the same way as $* will be in the recipe section, namely, exactly identical to the % expansion in the first expansion. This effectively gives us two instances of %, the one expanded in the first expansion, and $$* expanded in the second expansion. + +build-% also uses the same 'fake recipe' trick as push-%, that is, having a recipe that does nothing, to trick make into letting you run this. + +#### One Level Deeper + +The rules you've seen so far were intended for user interaction. they are all rules that the end user of this Makefile picks between, when deciding what they want this makefile to do. Let's look at the rules that these depend on. + +```bash +$ cat Makefile | sed -n -E '/^(manifest-push)/{:n;N;s/\n\t/\n /;tn;p}' +manifest-push-%: $$(foreach arch,$$(call goodarches,$$*), manifest-annotate-$$(arch)-$$*) + docker manifest push $(USERNAME)/$*$(TAGNAME) + +$ +``` + +manifest-push-% should be relatively simple for you now. the only thing new here, is you get to see $* used in the construction of our docker manifest push command line. Let's follow the manifest creation down a few more steps. + +```bash +$ cat Makefile | sed -n -E '/^(manifest-ann|manifest-crea)/{:n;N;s/\n\t/\n /;tn;p}' +manifest-annotate-%: manifest-create-$$(call snd,$$*) + docker manifest annotate $(USERNAME)/$(call snd,$*)$(TAGNAME) $(USERNAME)/$(call snd,$*)$(TAGNAME)-$(call fst,$*) --arch $(call maniarch,$(call fst,$*)) $(call manivariant,$(call fst,$*)) + +manifest-create-%: $$(foreach arch,$$(call goodarches,%), upload-$$(arch)-$$*) + docker manifest create $(USERNAME)/$*$(TAGNAME) $(patsubst %,$(USERNAME)/$*$(TAGNAME)-%,$(call goodarches,$*)) --amend + +``` + +manifest-push depends on manifest-annotate, which depends on manifest-create, that depends on upload-... so when make tries to push a manifest, it makes sure an image has been uploaded, then creates a manifest, then annotates the manifest. We're basically writing rules for each step of our manifest, only backwards. continuing this pattern, the last thing we will depend on will be the rules that actually download the dockerfiles from git. + +#### Dependency Resolving + +We've covered the entry points of this Makefile, and the chained dependencies that create, annotate, and upload a manifest file. now, we get into two seriously complicated sets of rules, the upload rules and the create rules. These accomplish their tasks of uploading and building docker containers, but at the same time, they accomplish our dependency resolution. Let's take a look. + +```bash +$ cat Makefile | sed -n -E '/^(upload|create|my-|dep)/{:n;N;s/\n\t/\n /;tn;p}' + +upload-%: create-% $$(foreach predep,$$(filter $$(call snd,%)-%,$$(PREBUILDS)), dep-upload-$$(call fst,$$*)-$$(call snd,$$(predep))) + docker push $(USERNAME)/$(call snd,$*)$(TAGNAME)-$(call fst,$*) | cat + +dep-upload-%: create-% $$(foreach predep,$$(filter $$(call snd,%)-%,$$(PREBUILDS)), dep-subupload-$$(call fst,$$*)-$$(call snd,$$(predep))) + docker push $(USERNAME)/$(call snd,$*)$(TAGNAME)-$(call fst,$*) | cat + +dep-subupload-%: create-% + docker push $(USERNAME)/$(call snd,$*)$(TAGNAME)-$(call fst,$*) | cat + +create-%: Dockerfile-$$(foreach target,$$(filter $$(call snd,$$*),$(NOMANIFEST)),NOMANIFEST-)$$* $$(foreach predep,$$(filter $$(call snd,%)-%,$(PREBUILDS)), depend-create-$$(call fst,$$*)-$$(call snd,$$(predep))) + cd $(call snd,$*) && docker build -t $(USERNAME)/$(call snd,$*)$(TAGNAME)-$(call fst,$*) -f Dockerfile-$(call fst,$*) . | cat + +depend-create-%: Dockerfile-$$(foreach target,$$(filter $$(call snd,$$*),$(NOMANIFEST)),NOMANIFEST-)$$* $$(foreach predep,$$(filter $$(call snd,%)-%,$(PREBUILDS)), depend-subcreate-$$(call fst,$$*)-$$(call snd,$$(predep))) + cd $(call snd,$*) && docker build -t $(USERNAME)/$(call snd,$*)$(TAGNAME)-$(call fst,$*) -f Dockerfile-$(call fst,$*) . | cat + +depend-subcreate-%: Dockerfile-$$(foreach target,$$(filter $$(call snd,$$*),$(NOMANIFEST)),NOMANIFEST-)$$* + cd $(call snd,$*) && docker build -t $(USERNAME)/$(call snd,$*)$(TAGNAME)-$(call fst,$*) -f Dockerfile-$(call fst,$*) . | cat + +$ +``` + +First, let's tackle the roles of these rules. the *upload* rules are responsible for running docker push, while the *create* rules are responsible for running docker build. All of the upload rules depend on the first create rule, to ensure what they want to run has been built. + +these rules are setup in groups of three: + +upload-% and create-% form the root of these groups. upload-% depends on create-%, and create-% depends on the creation of a Dockerfile for this image, which is the bottom of our dependency tree. + +upload-%/create-% depend on two rules: dep-upload-%/depend-create-%, which handle the upload/create for the image that THIS image depends on. There are also dep-subupload-% and dep-subcreate-% rules, to handle the dependency of the dependency of this image. + +This dependency-of, and dependency-of-dependency logic is necessary because Make will not let us run a recursive rule: no rule can be in one branch of the dependency graph more than once. so instead, the root of our dependency tree either starts with a single image, or with a list of images that are the root of their own dependency graphs. + + +Now let's look at the rules themselves. +upload-% has a dependency on create-%, to ensure what it wantas to upload already exists. additionally, it has a dependency that uses foreach and filter to look through the list of PREBUILDS, and depend on dep-upload-- for any images this image depends on. + +dep-upload-% is virtually identical to upload-%, also searching through PREBUILDS for possible dependencies, and depending on dep-subupload to build them. + +dep-subupload does no dependency search, but has an identical docker push recipe to upload, and dep-upload. + +create-%, depend-create-%, and depend-subcreate-% work similarly to the upload rules, calling docker build instead of a docker push, and depending on the Dockerfile having been created. When depending on the Dockerfile, we look through the NOMANIFEST list, and insert "NOMANIFEST-" in the name of dependency on the dockerfile. This is so that we depend on the NOMANIFEST variant if the image we are building requires us to use a postfix on the image name to access a version for a specified architecture. otherwise, we run the Dockerfile-% rule that uses a prefix (i386/, amd64/, etc) to access the docker image we are building from. + +It's worth noting that for all of these *create* and *upload* rules, we pipe the output of docker to cat, which causes docker to stop trying to draw progress bars. This seriously cleans up the + + +#### Building Dockerfiles. + +There are two rules for creating Dockerfiles, and we decide in the *create* rules which of these to use by looking at the NOMANIFEST variable, and adding -NOMANIFEST in the name of the rule we depend on for dockerfile creation. + +The rules are relatively simple: +```bash +$ cat Makefile | sed -n -E '/^(Dock)/{:n;N;s/\n\t/\n /;tn;p}' +Dockerfile-NOMANIFEST-%: $$(call snd,%)/Dockerfile + cd $(call snd,$*) && cat Dockerfile | ${SED} "s/^\(MAINTAINER\).*/\1 $(REALNAME) \"$(EMAIL)\"/" | ${SED} "s=^\(FROM \)\(.*\)$$=\1\2$(call archpostfix,$*)=" > Dockerfile-$(call fst,$*) + +Dockerfile-%: $$(call snd,%)/Dockerfile + cd $(call snd,$*) && cat Dockerfile | ${SED} "s/^\(MAINTAINER\).*/\1 $(REALNAME) \"$(EMAIL)\"/" | ${SED} "s=^\(FROM \)\(.*\)$$=\1$(call archpath,$(call fst,$*))\2=" > Dockerfile-$(call fst,$*) +$ +``` + +These two rules depend on the checkout of the git repos containing the Dockerfiles. they do this by depending on /Dockerfile. The rules are responsible for the creation of individual architecture specific derivitives of the Dockerfile that is downloaded. additionally, the rules set the MAINTAINER of the docker image to be us. Most of the heavy lifting of these rules is being done in the archpostfix, and archpath functions, which are being used in a sed expression to either postfix or prefix the image that this image is built from. + + +Let's take a look at that sed with a simpler example: + +SED ABUSE: +```bash +$ echo "FROM image-version" | sed "s=^\(FROM \)\(.*\)$=\1i386/\2=" +FROM i386/image-version +$ +``` + +Unlike our previous sed commands, which have all been forms of "look for this thing, and display it", with the 's' command basically being abused as a test, this one intentionally is making a change. + +'s' commands are immediately followed by a character, that is used to seperate and terminate two blocks of text: the part we're looking for (match section), and the part we're replacing it with(substitution section). Previously, we've used '/' as the character following a 's' command, but since we're using '/' in the text we're placing into the file, we're going to use the '=' character instead. We've covered the '^' character at the beginning of the pattern being an anchor for "this pattern should be found only at the begining of the line". In the match section of this command, we're introducing "$" as the opposite anchor: $ means "end of line.". we're not using a -E on the command line, so are forced to use "\" before our parenthesis for our matching functions. this is a pure stylistic decision. the .* in the second matching section stands for 'any character, any number of times', which will definately match against our dependent image name. + +The match section of this sed command basicaly translates to "at the beginning of the line, look for "FROM ", store it, and store anything else you find up to the end of the line.". These two store operations get placed in sed variables, named \1, and \2. a SED command can have up to nine variables, which we are using in the substitution section. + +The substitution section of this sed command uses the \1 and \2 variable references to wrap the string "i386/". this effectively places i386/ in front of the image name. + +Because we are using that sed command in a Makefile, we have to double up the "$" symbol, to prevent make from interpreting it as a variable. In the first sed command in these rules, we're also doing some minor escaping, adding a '\' in front of some quotes, so that our substitution of the maintainer has quotes around the email address. + +#### Downloading Dockerfiles + +Finally, we are at the bottom of our dependency tree. We've followed this is reverse order, but when we actually ask for things to be pushed, or to be built, these rules are the first ones run. + +There are a lot of these, of various complexities, so let's start with the simple ones first. + +##### Simple Checkout + +```bash +$ cat Makefile | sed -n -E '/^(smtp|dynamo|minio)/{:n;N;s/\n\t/\n /;tn;p}' +smtp/Dockerfile: + git clone https://github.com/namshi/docker-smtp.git smtp + cd smtp && git reset --hard $(SMTP_COMMIT) + +dynamodb_local/Dockerfile: + git clone https://github.com/cnadiminti/docker-dynamodb-local.git dynamodb_local + cd dynamodb_local && git reset --hard $(DYNAMODB_COMMIT) + +minio/Dockerfile: + git clone https://github.com/minio/minio.git minio + cd minio && git reset --hard $(MINIO_COMMIT) + +``` + +These rules are simple. They git clone a repo, then reset the repo to a known good revision. This isolates us from potential breakage from upstreams, and prevents someone from stealing git credentials for our upstreams, and using those credentials to make a malignant version. + +##### Checkout with Modifications + +A bit more complex rule is localstack/Dockerfile: +```bash +$ cat Makefile | sed -n -E '/^(localsta)/{:n;N;s/\n\t/\n /;tn;p}' +localstack/Dockerfile: + git clone https://github.com/localstack/localstack.git localstack + cd localstack && git reset --hard $(LOCALSTACK_COMMIT) + ${SED} -i.bak "s=localstack/java-maven-node-python=$(USERNAME)/java_maven_node_python$(TAGNAME)=" $@ + # skip tests. they take too long. + ${SED} -i.bak "s=make lint.*=make lint=" localstack/Makefile + ${SED} -i.bak "s=\(.*lambda.*\)=#\1=" localstack/Makefile + +$ +``` + +This rule makes some minor modifications to localstack's Dockerfile, and the Makefile that localstack's build process places in the docker image. It changes the Dockerfile such that instead of depending on upstream's version of the java-maven-node-python docker image, we depend on the version we are building. additionally, we disable the test cases for localstack, because they take a long time, and have a timing issues on emulators. It's worth noting that we use the make "$@" variable here, which evaluates to the build target, AKA, everything to the left of the ":" on the first line of our rule. + +SED ABUSE: +These have a little bit of new sed, for us. We're using the '-i' option to sed, to perform sed operations in place, which is to say, we tell sed to edit the file, and store a backup of the file before it edited it as .bak. Other than that, these are standard substitutions, like we covered in our previous SED ABUSE section. + +In the same approximate category is the java_maven_node_python/Dockerfile rule: +```bash +$ cat Makefile | sed -n -E '/^(java)/{:n;N;s/\n\t/\n /;tn;p}' +java_maven_node_python/Dockerfile: + git clone https://github.com/localstack/localstack.git java_maven_node_python + cd java_maven_node_python && git reset --hard $(JAVAMAVENNODEPYTHON_COMMIT) + cd java_maven_node_python && mv bin/Dockerfile.base Dockerfile + # disable installing docker-ce. not available on many architectures in binary form. + ${SED} -i.bak "/.*install Docker.*/{N;N;N;N;N;d}" $@ +``` + +This rule does a checkout like the localstack rule, but the Dockerfile is stored somewhere other that the root of the repo. we move the Dockerfile, then we disable the installation of docker-ce in the environment. we don't use it, and it's got problems with not being ported to all architectures. + +SED ABUSE: +To disable the installation of docker here, we do something a bit hacky. we find the line with 'install Docker' on it, we pull the next 5 lines into the pattern buffer, then delete them. This is effectively just a multiline delete. we use the -i.bak command line, just like the last sed abuse. neat and simple. + + +##### Checkout, Copy, Modify + +Some of the git repositories that we depend on do not store the Dockerfile in the root of the repository. instead, they have one large repository, with many directories containing many docker images. In these cases, we use git to check out the repository into a directory with the name of the image followed by '-all', then copy the directory we want out of the tree. + +```bash +$ cat Makefile | sed -n -E '/^(airdock)/{:n;N;s/\n\t/\n /;tn;p}' +airdock_base/Dockerfile: + git clone https://github.com/airdock-io/docker-base.git airdock_base-all + cd airdock_base-all && git reset --hard $(AIRDOCKBASE_COMMIT) + cp -R airdock_base-all/jessie airdock_base + # work around go compiler bug by using newer version of GOSU. https://bugs.launchpad.net/qemu/+bug/1696353 + ${SED} -i.bak "s/GOSU_VERSION=.* /GOSU_VERSION=1.11 /" $@ + # work around missing architecture specific binaries in earlier versions of tini. + ${SED} -i.bak "s/TINI_VERSION=.*/TINI_VERSION=v0.16.1/" $@ + # work around the lack of architecture usage when downloading tini binaries. https://github.com/airdock-io/docker-base/issues/8 + ${SED} -i.bak 's/tini\(.asc\|\)"/tini-\$$dpkgArch\1"/' $@ + +airdock_rvm/Dockerfile: + git clone https://github.com/airdock-io/docker-rvm.git airdock_rvm-all + cd airdock_rvm-all && git reset --hard $(AIRDOCKRVM_COMMIT) + cp -R airdock_rvm-all/jessie-rvm airdock_rvm + ${SED} -i.bak "s=airdock/base:jessie=$(USERNAME)/airdock_base$(TAGNAME)=" $@ + # add a second key used to sign ruby to the dockerfile. https://github.com/airdock-io/docker-rvm/issues/1 + ${SED} -i.bak "s=\(409B6B1796C275462A1703113804BB82D39DC0E3\)=\1 7D2BAF1CF37B13E2069D6956105BD0E739499BDB=" $@ + +airdock_fakesqs/Dockerfile: + git clone https://github.com/airdock-io/docker-fake-sqs.git airdock_fakesqs-all + cd airdock_fakesqs-all && git reset --hard $(AIRDOCKFAKESQS_COMMIT) + cp -R airdock_fakesqs-all/0.3.1 airdock_fakesqs + ${SED} -i.bak "s=airdock/rvm:latest=$(USERNAME)/airdock_rvm$(TAGNAME)=" $@ + # add a workdir declaration to the final switch to root. + ${SED} -i.bak "s=^USER root=USER root\nWORKDIR /=" $@ + # break directory creation into two pieces, one run by root. + ${SED} -i.bak "s=^USER ruby=USER root=" $@ + ${SED} -i.bak "s=cd /srv/ruby/fake-sqs.*=chown ruby.ruby /srv/ruby/fake-sqs\nUSER ruby\nWORKDIR /srv/ruby/fake-sqs\nRUN cd /srv/ruby/fake-sqs \&\& \\\\=" $@ +``` + +In airdock_base/Dockefile, we do a clone, set it to the revision we are expecting, then copy out one directory from that repo, creating an airdock_base/ directory containing a Dockerfile, like we expect. We then change out some version numbers in the Dockerfile to work around some known bugs, and do a minor modification to two commands to allow airdock_base to be built for non-amd64 architectures. + +SED ABUSE: +The sed in the airdock_base/Dockerfile rule is relatively standard fare for us now, with the exception of the last command. in it, we use a match against "\(.asc\|\)", meaning either a .asc, or empty string. This lets this sed command modify both the line that contains the path to the signature for tini, and the path to the tini package. Since we want a '$' in the dockerfile, so that when the dockerfile is run, it looks at it's internal '$dpkgArch' variable, we have to escape it with a $ to prevent make from eating it, and with a \ to prevent SED from trying to interpret it. + +In airdock_rvm/Dockerfile, we do the same clone, reset hard, copy routine as we did in airdock_base/Dockerfile. Since airdock_rvm depends on airdock_base, we change the image this image derives from to point to our airdock_base image. Additionally, to work around the image using an old signature to verify it's ruby download, we add another key to the gpg import line in the Dockerfile. Technically both keys are in use by the project now, so we did not remove the old one. + +airdock_fakesqs had a bit more modification that was required. we follow the same routine as in airdock_rvm/Dockerfile, doing our clone, reset, copy, and dependant image change, then we have to make some modifications to the WORKDIR and USERs in this Dockerfile. I don't know how they successfully build it, but it looks to me like their original file is using a different docker file interpreter, with a different permissions model. when we tried to run the Dockerfile, it would give us permissions errors. These changes make it function, by being a bit more explicit about creating things with the right permissions. + +SED ABUSE: +Let's take a look at the effect of these sed commands, before we dig into the commands themselves. + +```bash +$ diff -u airdock_fakesqs-all/0.3.1/Dockerfile airdock_fakesqs/Dockerfile +--- airdock_fakesqs-all/0.3.1/Dockerfile 2019-03-11 16:47:40.367319559 +0000 ++++ airdock_fakesqs/Dockerfile 2019-03-11 16:47:40.419320902 +0000 +@@ -4,15 +4,19 @@ + # TO_BUILD: docker build --rm -t airdock/fake-sqs . + # SOURCE: https://github.com/airdock-io/docker-fake-sqs + +-FROM airdock/rvm:latest ++FROM julialongtin/airdock_rvm:0.0.9 + MAINTAINER Jerome Guibert + ARG FAKE_SQS_VERSION=0.3.1 +-USER ruby ++USER root + +-RUN mkdir -p /srv/ruby/fake-sqs && cd /srv/ruby/fake-sqs && \ ++RUN mkdir -p /srv/ruby/fake-sqs && chown ruby.ruby /srv/ruby/fake-sqs ++USER ruby ++WORKDIR /srv/ruby/fake-sqs ++RUN cd /srv/ruby/fake-sqs && \ + rvm ruby-2.3 do gem install fake_sqs -v ${FAKE_SQS_VERSION} --no-ri --no-rdoc + + USER root ++WORKDIR / + + EXPOSE 4568 +``` + +The first change is our path change, to use the airdock_rvm image we're managing, instead of upstream's latest. +The second and third change happens at the place in this file where it fails. On my machine, the mkdir fails, as the ruby user cannot create this directory. to solve this, we perform the directory creation an root, THEN do our rvm work. + +Now, let's look through the sed that did that. +The first sed command in this rule changed the path on the FROM line, just like the similar sed statement in the last make rule we were looking at. +The second sed command added a 'WORKDIR /' to the bottom of the Dockerfile, after the USER root. +The third SED command changes the USER line at the top of the file to using the root user to run the next command, instead of the ruby user. +Finally, the fourth SED command changes the first RUN command into two run commands. one creates the directory and makes sure we have permissions to it, while the second runs our command. the sed command also inserts commands to change user to ruby, and change working directories to the directory created in the first RUN command. + +Structurally, the first, second, and third sed command are all pretty standard things we've seen before. The fourth command looks a little different, but really, it's the same sort of substitution, only it adds several lines. At the end of the statement is some tricky escaping. +'&' characters must be escaped, because in sed, an '&' character is shorthand for 'the entire patern that we matched'. That will be important, later. the single '\' character has to be escaped into '\\\\'. + +Note that when we wrote our 'clean' rule, we added these '-all' directories manually, to make sure they would get deleted. + +##### Checkout, Copy, Modify Multiline + +elasticsearch and cassandra's checkouts are complicated, as they do a bit of injection of code into the docker entrypoint script. The entrypoint script is the script that is launched when you run a docker image. It's responsible for reading in environment variables, setting up the service that the docker image is supposed to run, and then running the service. For both elasticsearch and cassandra, we do a multiline insert, and we do it with multiple chained commands. + +Let's look at elasticsearch, as these two rules are almost identical. + +```bash +$ cat Makefile | sed -n -E '/^(ela)/{:n;N;s/\n\t/\n /;tn;p}' +elasticsearch/Dockerfile: + git clone https://github.com/blacktop/docker-elasticsearch-alpine.git elasticsearch-all + cd elasticsearch-all && git reset --hard $(ELASTICSEARCH_COMMIT) + cp -R elasticsearch-all/5.6/ elasticsearch + # add a block to the entrypoint script to interpret CS_JVM_OPTIONS, modifying the jvm.options before launching elasticsearch. + # first, add a marker to be replaced before the last if. + ${SED} -i.bak -r ':a;$$!{N;ba};s/^(.*)(\n?)fi/\2\1fi\nREPLACEME/' elasticsearch/elastic-entrypoint.sh + # next, load our variables. + ${SED} -i.bak 's@REPLACEME@MY_APP_CONFIG="/usr/share/elasticsearch/config/"\n&@' elasticsearch/elastic-entrypoint.sh + # add our parser and replacer. + ${SED} -i.bak $$'s@REPLACEME@if [ ! -z "$${JVM_OPTIONS_ES}" ]; then\\nfor x in $${JVM_OPTIONS_ES}; do { l="$${x%%=*}"; r=""; e=""; [ "$$x" != "$${x/=//}" ] \&\& e="=" \&\& r="$${x##*=}"; [ "$$x" != "$${x##-Xm?}" ] \&\& r="$${x##-Xm?}" \&\& l="$${x%%$$r}"; echo $$l $$e $$r; sed -i.bak -r \'s/^[# ]?(\'"$$l$$e"\').*/\\\\1\'"$$r"\'/\' "$$MY_APP_CONFIG/jvm.options"; diff "$$MY_APP_CONFIG/jvm.options.bak" "$$MY_APP_CONFIG/jvm.options" \&\& echo "no difference"; } done;\\nfi\\n&@' elasticsearch/elastic-entrypoint.sh + # remove the marker we added earlier. + ${SED} -i.bak 's@REPLACEME@@' elasticsearch/elastic-entrypoint.sh + +$ +``` + +In this rule, we're checking out the git tree, and copying one directory that contains our Dockerfile, and our entrypoint for elasticsearch. Following that, we have four sed commands, one of which inserts some very complicated bash. + +SED ABUSE: +Our first sed command in this rule uses a new trick. We're using -i to edit in place, and -r to quash output. Instead of starting with a match(/.../) or a substitution (s/thing/otherthing/), we immediately start with a label. let's break down this command. + +```sed +:a; # an anchor, we can loop back to. +$!{ # enter here only if there is content to be read from the file. note that to get this "$" past make, we had to escape it, by replacing it with $$. +N; # pull in the next line of content into the pattern space +ba # branch to the 'a' label. +}; +s/(.*)(\n?)fi/\2\1fi\nREPLACEME/ #match everything up to the last 'fi' and replace it with a 'fi', a new line, and REPLACEME +``` + +What does that effectively do? the source file contains a lot of lines with 'fi' in them, by inserting REPLACEME after the last one, this gives us an anchor point, that we can safely run simpler sed commands against. + +for instance, our next sed command: +```sed +s@REPLACEME@MY_APP_CONFIG="/usr/share/elasticsearch/config/"\n&@ +``` + +the 's' on this command is using '@' symbols to seperate the pattern from the replacement. it operates by finding the 'REPLACEME' that we inserted with the last command. As we touched on earlier, the unescaped '&' at the end of this replacement repeats back the patern, in the replacement. This effectively means that this line replaces REPLACEME with a new line of code, and puts the REPLACEME after the line it inserted. + +BASH ABUSE: +The next sed command works similarly, however it inserts an extremely complicated pile of bash on one line. Let's take a look at it. I'm going to remove some semicolons, remove some of the escaping, and insert line breaks and comments, to make this a bit more readable. +```bash +if [ ! -z "$${JVM_OPTIONS_ES}" ]; then # only if JVM_OPTIONS_ES was set when docker was run + for x in $${JVM_OPTIONS_ES} + do { + # set l to everything to the left of an equal sign. + l="${x%%=*}" + # clear out r and e. + r="" + e="" + # if there was an equal sign, set e to an equal sign, and set r to everything after the equal sign. + [ "$x" != "${x/=//}" ] && e="=" && r="$${x##*=}" + # if there was an '-Xm' (a java memory option), set r to the content after the (-XM), and set l to the -XM + [ "$x" != "${x##-Xm?}" ] && r="$${x##-Xm?}" && l="${x%%$r}" + # debugging code. echo what we saw. + echo $l $e $r + # perform a substitution, uncommenting a line found that starts with $l$e, and replacing it with $l$e$r. + sed -i.bak -r 's/^[# ]?('"$l$e"').*/\1'"$r"'/' "$MY_APP_CONFIG/jvm.options" + # show that a change was done with diff, or say there was no difference. + diff "$$MY_APP_CONFIG/jvm.options.bak" "$MY_APP_CONFIG/jvm.options" && echo "no difference"; + } done; +fi +``` + +What this bash script is doing is, it looks for a JVM_OPTIONS_ES environment variable, and if it finds it, it rewrites the jvm.options file, uncommenting and replacing the values for java options. This allows us to change the memory pool settings, and possibly other settings, by setting a variable in the docker compose file that starts up our integration test. + +This bash script is inserted by a sed command and CONTAINS a sed command, and lots of special characters. The quoting of this is handled a bit differently: instead of just surrounding our sed command in '' characters, we use $'', which is bash for "use C style escaping here". + +SED ABUSE: +the bash script above uses a relatively normal sed command, but intersparces it with ' and " characters, in order to pass the sed command in groups of '' characters, while using "" around the sections that we have variables in. bash will substitute variables in doublequotes, but will not substitute them in single quotes. +This substitution command uses slashes as its separators. it starts by anchoring to the beginning of the line, and matching against either a single '#' character, or a single space. it does this by grouping the space and # in a character class ([ and ]), then using a question mark to indicate "maybe one of these.". the substitution continues by matching the bash variables $l and $e, saving them in \1, matching (and therefore removing) anything else on the line, and replacing the line with \1, followed immediately by the contents of the bash variable $r. + +The cassandra/Dockerfile rule is almost identical to this last rule, only substituting out the name of the variable we expect from docker to CS_JVM_OPTIONS, and changing the path to the jvm.options file. + +# Pitfalls I fell into writing this. + +The first large mistake I made when writing this, is that the root of the makefile's dependency tree contained both images that had dependencies, and the dependent images themselves. This had me writing methods to keep the image build process from stepping on itsself. what was happening is that, in the case of the airdock-* and localstack images, when trying to build all of the images at once, make would race all the way down to the git clone steps, and run the git clone multiple times at the same time, where it just needs to be run once. + +The second was that I didn't really understand that manifest files refer to dockerhub only, not to the local machine. This was giving me similar race conditions, where an image build for architecture A would complete, and try to build the manifest when architecture B was still building. + +The third was writing really complicated SED and BASH and MAKE. ;p \ No newline at end of file diff --git a/docs/legacy/reference/provisioning/scim-token.md b/docs/legacy/reference/provisioning/scim-token.md new file mode 100644 index 00000000000..b7ae891de53 --- /dev/null +++ b/docs/legacy/reference/provisioning/scim-token.md @@ -0,0 +1,114 @@ +# SCIM tokens {#RefScimToken} + +_Author: Artyom Kazak_ + +--- + +A _SCIM token_ is a bearer token used to authorize SCIM operations. + +A team owner can create SCIM tokens for the team. Each of those tokens can be used to provision new members to the team, modify members' profile attributes, etc. Tokens have unlimited duration, but can be revoked. + +## Using a SCIM token {#RefScimTokenUsage} + +SCIM tokens are not general-purpose API tokens. They only apply to the `/scim/v2/` subtree of the API. + +SCIM tokens are intended to be used by provisioning tools, such as Okta, OneLogin, Active Directory, and so on. If you have your own provisioning utility, you can use a token by adding an `Authorization` header to all `/scim/v2/` requests: + +``` +Authorization: Bearer +``` + +A SCIM token identifies the team that the token belongs to, so you do not need to specify the team in the request. + +## API {#RefScimTokenApi} + +### Creating a token {#RefScimTokenCreate} + +Creating a token requires the user to be a team owner. As an additional precaution, we also require the user to re-enter their password. + +There is a reasonable limit on the number of tokens a single team can have, set in `scim.yaml` at `maxScimTokens`. For Wire the limit is 16. + +Sample request and response: + +``` +POST /scim/auth-tokens + +{ + // Token description (useful as a memory aid; non-optional but can be empty) + "description": "Sample token", + + // User password. If the user does not have a password (e.g. they are + // SCIM-provisioned), this field should be omitted. + "password": "secret" +} +``` + +``` +201 Created + +{ + // Token itself; only sent once, can not be listed + "token": "MzIgcmFuZG9tIGJ5dGVzIGluIGJhc2U2NCBmb3JtYXQ=", + + // Metadata about the token, can be listed + "info": { + // Token ID, can be used to revoke the token + "id": "514613de-9fde-4aab-a704-c57af7a3366e", + // Token description + "description": "Sample token", + // Team associated with the token + "team": "78c65523-490f-4e45-881f-745e3458e280", + // When the token was created + "created_at": "2019-04-18T14:09:43.732Z", + // Identity provider for users provisioned with the token + // (optional but for now always present) + "idp": "784edde5-29d8-4dc3-bb95-924519448f09" + } +} +``` + +Note that SCIM can only be used with teams that have either no or exactly one SAML IdP ([internal issue](https://github.com/zinfra/backend-issues/issues/1377)). + +### Listing existing tokens {#RefScimTokenList} + +Listing tokens requires the user to be a team owner. + +We don't ever send tokens themselves, only the metadata (which can be used, for instance, to decide which tokens to revoke). + +Sample request and response: + +``` +GET /scim/auth-tokens +``` + +``` +200 OK + +{ + "tokens": [ + { + "id": "514613de-9fde-4aab-a704-c57af7a3366e", + "description": "Sample token", + "team": "78c65523-490f-4e45-881f-745e3458e280", + "created_at": "2019-04-18T14:09:43.732Z", + "idp": "784edde5-29d8-4dc3-bb95-924519448f09" + } + ] +} +``` + +### Revoking a token {#RefScimTokenDelete} + +Revoking a token requires the user to be a team owner. + +To revoke a token, the user has to provide the token ID (not the token itself). The revoked token becomes unused immediately and does not show up in the results of `GET /scim/auth-tokens`. + +Sample request and response: + +``` +DELETE /scim/auth-tokens?id=514613de-9fde-4aab-a704-c57af7a3366e +``` + +``` +204 No Content +``` diff --git a/docs/legacy/reference/provisioning/scim-via-curl.md b/docs/legacy/reference/provisioning/scim-via-curl.md new file mode 100644 index 00000000000..aaa2a9eea56 --- /dev/null +++ b/docs/legacy/reference/provisioning/scim-via-curl.md @@ -0,0 +1 @@ +# This page [has gone here](https://docs.wire.com/understand/single-sign-on/main.html#using-scim-via-curl). diff --git a/docs/legacy/reference/provisioning/wire_scim_token.py b/docs/legacy/reference/provisioning/wire_scim_token.py new file mode 100755 index 00000000000..2823a7bbca8 --- /dev/null +++ b/docs/legacy/reference/provisioning/wire_scim_token.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# +# This file is part of the Wire Server implementation. +# +# Copyright (C) 2020 Wire Swiss GmbH +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see . + +from __future__ import print_function + +# NOTE: This python script requires the "requests" library to be installed. + +# Change this if you are running your own instance of Wire. +BACKEND_URL='https://prod-nginz-https.wire.com' + +import sys +import getpass +from requests import Request, Session +import requests +import json +import datetime + +session = None + +def init_session(): + global session + session = Session() + session.headers.update({'User-Agent': "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"}) + +def post(url): + return Request('POST', url) + +def get(url): + return Request('GET', url) + +def set_bearer_token(request, token): + request.headers['Authorization'] = 'Bearer ' + token + +def backend(path): + return BACKEND_URL + path + +def has_json_content(response): + content_type = response.headers.get('Content-Type') + if content_type is not None: + return (content_type.startswith('application/json') + or content_type == 'application/scim+json;charset=utf-8') + else: + return False + +def send_request(session, request): + response = session.send(session.prepare_request(request)) + if 200 <= response.status_code and response.status_code < 300: + if has_json_content(response): + return response.json() + else: + return response + else: + print(f"Request failed {request.url}", file=sys.stderr) + if has_json_content(response): + tpl = response, response.json() + else: + tpl = response, response.content + print(tpl, file=sys.stderr) + exit(1) + +def create_bearer(email, password): + r = post(backend('/login?persist=false')) + r.headers['Accept'] = 'application/json' + r.json = {'email': email, 'password': password} + return send_request(session, r) + +def create_scim_token(admin_password, token): + r = post(backend('/scim/auth-tokens')) + set_bearer_token(r, token) + r.json = {'description': 'token generated at ' + datetime.datetime.now().isoformat(), + 'password': admin_password + } + return send_request(session, r) + +def exit_fail(msg): + print(msg, file=sys.stderr) + exit(1) + +def main(): + init_session() + print('This script generates a token that authorizes calls to Wire\'s SCIM endpoints.\n') + print('Please enter the login credentials of a user that has role "owner" or "admin".') + ADMIN_EMAIL=input("Email: ") or exit_fail('Please provide an email.') + ADMIN_PASSWORD=getpass.getpass('Password: ') or exit_fail('Please provide password.') + bearer_token = create_bearer(ADMIN_EMAIL, ADMIN_PASSWORD) + scim_token = create_scim_token(ADMIN_PASSWORD, bearer_token['access_token']) + print('Wire SCIM Token: ' + scim_token['token']) + print('The token will be valid until you generate a new token for this user.') + +if __name__ == '__main__': + main() diff --git a/docs/legacy/reference/spar-braindump.md b/docs/legacy/reference/spar-braindump.md new file mode 100644 index 00000000000..f92775becaf --- /dev/null +++ b/docs/legacy/reference/spar-braindump.md @@ -0,0 +1,405 @@ +# Spar braindump {#SparBrainDump} + +_Author: Matthias Fischmann_ + +--- + +# the spar service for user provisioning (scim) and authentication (saml) - a brain dump + +this is a mix of information on inmplementation details, architecture, +and operation. it should probably be sorted into different places in +the future, but if you can't find any more well-structured +documentation answering your questions, look here! + + +## related documentation + +- [list of howtos for supported SAML IdP vendors](https://docs.wire.com/how-to/single-sign-on/index.html) +- [fragment](https://docs.wire.com/understand/single-sign-on/design.html) (TODO: clean up the section "common misconceptions" below and move it here.) +- [official docs for team admin from customer support](https://support.wire.com/hc/en-us/categories/360000248538?section=administration%3Fsection%3Dadministration) (skip to "Authentication") +- [talk scim using curl](https://github.com/wireapp/wire-server/blob/develop/docs/reference/provisioning/scim-via-curl.md) +- if you want to work on our saml/scim implementation and do not have access to [https://github.com/zinfra/backend-issues/issues?q=is%3Aissue+is%3Aopen+label%3Aspar] and [https://github.com/wireapp/design-specs/tree/master/Single%20Sign%20On], please get in touch with us. + + +## design considerations + +### SCIM without SAML. + +Before https://github.com/wireapp/wire-server/pull/1200, scim tokens could only be added to teams that already had exactly one SAML IdP. Now, we also allow SAML-less teams to have SCIM provisioning. This is an alternative to onboarding via team-settings and produces user accounts that are authenticated with email and password. (Phone may or may not work, but is not officially supported.) + +The way this works is different from team-settings: we don't send invites, but we create active users immediately the moment the SCIM user post is processed. The new thing is that the created user has neither email nor phone nor a SAML identity, nor a password. + +How does this work? + +**email:** If no SAML IdP is present, SCIM user posts must contain an externalId that is an email address. This email address is not added to the newly created user, because it has not been validated. Instead, the flow for changing an email address is triggered in brig: an email is sent to the address containing a validation key, and once the user completes the flow, brig will add the email address to the user. We had to add very little code for this in this PR, it's all an old feature. + +When SCIM user gets are processed, in order to reconstruct the externalId from the user spar is retrieving from brig, we introduce a new json object for the `sso_id` field that looks like this: `{'scim_external_id': 'me@example.com'}`. + +In order to find users that have email addresses pending validation, we introduce a new table in spar's cassandra called `scim_external_ids`, in analogy to `user`. We have tried to use brig's internal `GET /i/user&email=...`, but that also finds pending email addresses, and there are corner cases when changing email addresses and waiting for the new address to be validated and the old to be removed... that made this approach seem infeasible. + +**password:** once the user has validated their email address, they need to trigger the "forgot password" flow -- also old code. + + +## operations + +### enabling / disabling the sso feature for a team + +if you have sso disabled by default, you need to turn on the feature +for every team that wants to use it. you can do this in the stern +service (aka backoffice). look for `get/put +/teams/{tid}/features/sso` + + +### registering an idp with a team via curl + +you need to have: + +```sh +# user id of an admin of the team (or the creator from the team info +# in the backoffice, if you only have the team id). +export ADMIN_ID=... + +# path of the xml metadata file (if you only have the url, curl it) +export METADATA_FILE=... +``` + +copy these two files to one of your spar instances: + +- `.../wire-server/deploy/services-demo/register_idp_internal.sh` +- `${METADATA_FILE}` + +... and ssh into it. then: + +```sh +./register_idp_internal.sh metadata.xml ${TEAM_OWNER_ID} +``` + +the output contains the a json object representing the idp. construct +the login code from the `id` field of that object by adding `wire-` in +front, eg.: + +``` +wire-e97fbe2e-eeb1-11e9-acf3-9ba77d8a04bf +``` + +give this login code to the users that you want to connect to wire +using this idp. see +[here](https://support.wire.com/hc/en-us/articles/360000954617-Login-with-SSO) +on how to use the login code. + + +### updating an idp via curl + +Your IdP metadata may change over time (eg., because a certificate +expires, or because you change vendor and all the metadata looks +completely different), these changes need to be updated in wire. We +offer two options to do that. + +Like when creating your first IdP, for both options you need define a +few things: + +``` +# user id of an admin of the team (or the creator from the team info +# in the backoffice, if you only have the team id). +export ADMIN_ID=... + +# path of the xml metadata file (if you only have the url, curl it) +export METADATA_FILE=... + +# The ID of the IdP you want to update (login code without the `wire-` +# prefix, which is a UUIDv4): +export IDP_ID=... +``` + +Copy the new metadata file to one of your spar instances. + +Ssh into it. If you can't, [the scim +docs](provisioning/scim-via-curl.md) explain how you can create a +bearer token if you have the admin's login credentials. If you follow +that approach, you need to replace all mentions of `-H'Z-User ...'` +with `-H'Authorization: Bearer ...'` in the following, and you won't need +`$ADMIN_ID`, but something like `$BEARER`. + +There are two ways to update an IDP, described below, each with their own tradeoffs that affect users. + +#### Option 1: Update the existing IdP in-place + +Effects: + +- You keep the login code, no visible changes for your users. +- The old IdP becomes immediately unaccessible. It will disappear + from team-settings, and users will have no way of using it for + authentication. +- If the has no account on the new IdP, she won't be able to login. +- If a user has an account on the new IdP, *but not with exactly the + same user name* (SAML NameID), she will not be logged in to their + old account. Instead, depending on your setup, a second account is + created for them, or they are blocked (both not what you want). + +```shell +curl -v \ + -XPUT http://localhost:8080/identity-providers/${IDP_ID} \ + -H"Z-User: ${ADMIN_ID}" \ + -H'Content-type: application/xml' \ + -d@"${METADATA_FILE}" +``` + +#### Option 2: Create a second IdP, and mark it as replacing the old one. + +Effects: + +- The new IdP will have a new login code. Users need to be invited to + use this new login code. +- If they use the old login code, they can keep using the old IdP as + long as it is still running as before. (This option is good for + smooth transitions from one IdP to the other.) +- If they use the new login code, they will be automatically moved + from the old IdP to the new one. After that, they won't be able to + use the old one any more. +- If a user logs into the team for the first time using the old login + code, no user will be created. The old IdP is marked as "replaced", + and wire only authenticates existing users with it. +- This doesn't currently work if you are using SCIM for provisioning, + because SCIM requires you to have exactly one IdP configured in your + wire team. (Internal details: + https://github.com/zinfra/backend-issues/issues/1365, + https://github.com/zinfra/backend-issues/issues/1377). +- If you go to team settings, you will see the old IdP and the new + one, and there is currently no way to distinguish between replaced + and active IdPs. (Internal details: + https://github.com/wireapp/wire-team-settings/issues/3465). + +```shell +curl -v \ + -XPOST http://localhost:8080/identity-providers'?replaces='${IDP_ID} \ + -H"Z-User: ${ADMIN_ID}" \ + -H'Content-type: application/xml' \ + -d@"${METADATA_FILE}" +``` + + +### deleting an idp via curl + +Read the beginning of the last section up to "Option 1". You need +`ADMIN_ID` (or `BEARER`) and `IDP_ID`, but not `METADATA_FILE`. + +```shell +curl -v + -XDELETE http://localhost:8080/identity-providers/${IDP_ID} \ + -H"Z-User: ${ADMIN_ID}" \ + -H'Content-type: application/json +``` + +If there are still users in your team with SAML credentials associated +with this IdP, you will get an error. You can either move these users +elsewhere, delete them manually, or purge them implicitly during +deletion of the IdP: + +```shell +curl -v + -XDELETE http://localhost:8080/identity-providers/${IDP_ID}?purge=true \ + -H"Z-User: ${ADMIN_ID}" \ + -H'Content-type: application/json +``` + +Haskell code: https://github.com/wireapp/wire-server/blob/d231550f67c117b7d100c7c8c6c01b5ad13b5a7e/services/spar/src/Spar/API.hs#L217-L271 + + +### setting a default SSO code + +To avoid having to give users the login code, a backend can also provide a default code on the endpoint `/sso/settings`. +This needs to be set explicitly, since this is not always wanted and there might even be multiple idps (each with their own login code): + +``` +curl -XPUT ${API_URL}/i/sso/settings -H 'Content-Type: application/json' -d '{"default_sso_code":"e97fbe2e-eeb1-11e9-acf3-9ba77d8a04bf"}' +``` + +Note the lack of the `wire-` prefix. + +This entry gets removed automatically when the corresponding idp is deleted. You can manually delete the default by making a `PUT` request as shown above with payload `{"default_sso_code":null}`. + +Clients can then ask for the default SSO code on `/sso/settings` and use it to initiate single sign-on. + + +### troubleshooting + +#### gathering information + +- find metadata for team in table `spar.idp_raw_metadata` via cqlsh + (since https://github.com/wireapp/wire-server/pull/872) + +- ask user for screenshots of the error message, or even better, for + the text. the error message contains lots of strings that you can + grep for in the spar sources. + + +#### making spar work with a new IdP + +often, new IdPs work out of the box, because there appears to be some +consensus about what minimum feature set everybody should support. + +if there are problems: collect the metadata xml and an authentication +response xml (either from the browser http logs via a more technically +savvy customer; FUTUREWORK: it would be nice to log all saml response +xml files that spar receives in prod and cannot process). + +https://github.com/wireapp/saml2-web-sso supports writing [unit vendor +compatibility +tests](https://github.com/wireapp/saml2-web-sso/blob/ff9b9f445475809d1fa31ef7f2932caa0ed31613/test/Test/SAML2/WebSSO/APISpec.hs#L266-L329) +against that response value. once that test passes, it should all +work fine. + + +### common misconceptions + + +#### an email address can be one of two things + +When users are SAML-authenticated with an email address under NameID, +that email address is used by wire as an opaque identifier, not to +send actual emails. In order to *also* assign the user that email +address, you can enable the feature flag `validateSAMLemails`. This +will trigger the regular email validation flow that is also triggered +when the user changes their email themselves. + + +#### scim, provisioning, metadata + +for changing the user information (name, handle, email, ...), saml +isn't enough. the identity management software (AD in this case, or +some add-on) needs to support scim. we *could* support doing that via +saml, but the part of the standards that are needed for that are even +in worse shape than the ones for the authentication bits, and it would +not lead to a good user experience. so instead we require users to +adopt the more robust and contemporary scim standard. + + +#### we don't support binding password/phone-auth'ed users to saml yet + +to keep track of whether we have, see https://github.com/zinfra/backend-issues/issues/731 + + + +## application logic + +### deleting users that exist on spar + +For scim- or saml-created users, there are three locations for user data: + +- `brig.user` (and a few things associated with that on brig and galley) +- `spar.user` +- `spar.scim_user` + +The single source of truth is `brig.user`. Dangling entries in the +other places are allowed, and must be checked by the application code +for danglingness. ([test case for +scim](https://github.com/wireapp/wire-server/blob/010ca7e460d13160b465de24dd3982a397f94c16/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs#L239-L308); +[test case for +saml](https://github.com/wireapp/wire-server/blob/293518655d7bae60fbcb0c4aaa06034785bfb6fc/services/spar/test-integration/Test/Spar/APISpec.hs#L742-L795)) + +For the semantics of interesting corner cases, consult [the test +suite](https://github.com/wireapp/wire-server/blob/develop/services/spar/test-integration/Test/Spar/APISpec.hs). +If you can't find what you're looking for there, please add at least a +pending test case explaining what's missing. + +Side note: Users in brig carry an enum type +[`ManagedBy`](https://github.com/wireapp/wire-server/blob/010ca7e460d13160b465de24dd3982a397f94c16/libs/brig-types/src/Brig/Types/Common.hs#L393-L413). This is a half-implemented feature for +managing conflicts between changes via scim vs. changes from wire +clients; and does currently not affect deletability of users. + + +#### delete via deleting idp + +[Currently](https://github.com/wireapp/wire-server/blob/d231550f67c117b7d100c7c8c6c01b5ad13b5a7e/services/spar/src/Spar/API.hs#L217-L271), we only have the rest API for this. Team settings will follow with a button. + + +#### user deletes herself + +TODO + + +#### delete in team settings + +TODO (probably little difference between this and "user deletes herself"?) + + +#### delete via scim + +TODO + + +## using the same IdP (same entityID, or Issuer) with different teams + +Some SAML IdP vendors do not allow to set up fresh entityIDs (issuers) +for fresh apps; instead, all apps controlled by the IdP are receiving +SAML credentials from the same issuer. + +In the past, wire has used the a tuple of IdP issuer and 'NameID' +(Haskell type 'UserRef') to uniquely identity users (tables +`spar.user_v2` and `spar.issuer_idp`). + +In order to allow one IdP to serve more than one team, this has been +changed: we now allow to identity an IdP by a combination of +entityID/issuer and wire `TeamId`. The necessary tweaks to the +protocol are listed here. + +For everybody using IdPs that do not have this limitation, we have +taken great care to not change the behavior. + + +### what you need to know when operating a team or an instance + +No instance-level configuration is required. + +If your IdP supports different entityID / issuer for different apps, +you don't need to change anything. We hope to deprecate the old +flavor of the SAML protocol eventually, but we will keep you posted in +the release notes, and give you time to react. + +If your IdP does not support different entityID / issuer for different +apps, keep reading. At the time of writing this section, there is no +support for multi-team IdP issuers in team-settings, so you have two +options: (1) use the rest API directly; or (2) contact our customer +support and send them the link to this section. + +If you feel up to calling the rest API, try the following: + +- Use the above end-point `GET /sso/metadata/:tid` with your `TeamId` + for pulling the SP metadata. +- When calling `POST /identity-provider`, make sure to add + `?api_version=v2`. (`?api_version=v1` or no omission of the query + param both invoke the old behavior.) + +NB: Neither version of the API allows you to provision a user with the +same Issuer and same NamdID. RATIONALE: this allows us to implement +'getSAMLUser' without adding 'TeamId' to 'UserRef', which in turn +would break the (admittedly leaky) abstarctions of saml2-web-sso. + + +### API changes in more detail + +- New query param `api_version=` for `POST + /identity-providers`. The version is stored in `spar.idp` together + with the rest of the IdP setup, and is used by `GET + /sso/initiate-login` (see below). +- `GET /sso/initiate-login` sends audience based on api_version stored + in `spar.idp`: for v1, the audience is `/sso/finalize-login`; for + v2, it's `/sso/finalize-login/:tid`. +- New end-point `POST /sso/finalize-login/:tid` that behaves + indistinguishable from `POST /sso/finalize-login`, except when more + than one IdP with the same issuer, but different teams are + registered. In that case, this end-point can process the + credentials by discriminating on the `TeamId`. +- `POST /sso/finalize-login/:tid` remains unchanged. +- New end-point `GET /sso/metadata/:tid` returns the same SP metadata as + `GET /sso/metadata`, with the exception that it lists + `"/sso/finalize-login/:tid"` as the path of the + `AssertionConsumerService` (rather than `"/sso/finalize-login"` as + before). +- `GET /sso/metadata` remains unchanged, and still returns the old SP + metadata, without the `TeamId` in the paths. + + +### database schema changes + +[V15](https://github.com/wireapp/wire-server/blob/b97439756cfe0721164934db1f80658b60de1e5e/services/spar/schema/src/V15.hs#L29-L43) diff --git a/docs/reference/team/legalhold.md b/docs/legacy/reference/team/legalhold.md similarity index 100% rename from docs/reference/team/legalhold.md rename to docs/legacy/reference/team/legalhold.md diff --git a/docs/legacy/reference/user/activation.md b/docs/legacy/reference/user/activation.md new file mode 100644 index 00000000000..60868cd5811 --- /dev/null +++ b/docs/legacy/reference/user/activation.md @@ -0,0 +1,152 @@ +# Activation {#RefActivation} + +_Author: Artyom Kazak_ + +--- + +A user is called _activated_ they have a verified identity -- e.g. a phone number that has been verified via a text message, or an email address that has been verified by sending an activation code to it. + +A user that has been provisioned via single sign-on is always considered to be activated. + +## Activated vs. non-activated users {#RefActivationBenefits} + +Non-activated users can not [connect](connection.md) to others, nor can connection requests be made to anonymous accounts from verified accounts. As a result: + +* A non-activated user cannot add other users to conversations. The only way to participate in a conversation is to either create a new conversation with link access or to use a link provided by another user. + +The only flow where it makes sense for non-activated users to exist is the [wireless flow](registration.md#RefRegistrationWireless) used for [guest rooms](https://wire.com/en/features/encrypted-guest-rooms/) + +## API {#RefActivationApi} + +### Requesting an activation code {#RefActivationRequest} + +During the [standard registration flow](registration.md#RefRegistrationStandard), the user submits an email address or phone number by making a request to `POST /activate/send`. A six-digit activation code will be sent to that email address / phone number. Sample request and response: + +``` +POST /activate/send + +{ + // Either 'email' or 'phone' + "phone": "+1234567890" +} +``` + +``` +200 OK +``` + +The user can submit the activation code during registration to prove that they own the email address / phone number. + +The same `POST /activate/send` endpoint can be used to re-request an activation code. Please use this ability sparingly! To avoid unnecessary activation code requests, users should be warned that it might take up to a few minutes for an email or text message to arrive. + +### Activating an existing account {#RefActivationSubmit} + +If the account [has not been activated during verification](registration.md#RefRegistrationNoPreverification), it can be activated afterwards by submitting an activation code to `POST /activate`. Sample request and response: + +``` +POST /activate + +{ + // One of 'phone', 'email', or 'key' + "phone": "+1234567890", + + // 6-digit activation code + "code": "123456", + + // Verify the 'code' but don't activate the account (the 3-attempt limit + // on failed verification attempts still applies) + "dryrun": false +} +``` + +``` +200 OK + +{ + "phone": "+1234567890", + + // Whether it is the first successful activation for the user + "first": true +} +``` + +If the email or phone has been verified already, `POST /activate` will return status code `204 No Content`. If the code is invalid, `POST /activate` will return status code `404 Not Found` with `"label": "invalid-code"`. + +There is a maximum of 3 activation attempts per activation code. On the third failed attempt the code is invalidated and a new one must be requested. + +### Activation event {#RefActivationEvent} + +When the user becomes activated, they receive an event: + +```json +{ + "type": "user.activate", + "user": +} +``` + +### Detecting activation in the self profile {#RefActivationProfile} + +In addition to the [activation event](#RefActivationEvent), activation can be detected by polling the self profile: + +``` +GET /self + +{ + "accent_id": 0, + "assets": [], + "email": "pink@example.com", + "id": "2f7e582b-9d99-4d50-bbb0-e659d63491d9", + "locale": "en", + "managed_by": "wire", + "name": "Pink", + "picture": [] +} +``` + +If the profile includes `"email"` or `"phone"`, the account is activated. + +## Automating activation via email {#RefActivationEmailHeaders} + +Our email verification messages contain headers that can be used to automate the activation process. + +An email caused by `POST /activate/send` will contain this set of headers: + +``` +X-Zeta-Purpose: Verification +X-Zeta-Code: 123456 +``` + +An email caused by `POST /register` will contain this set of headers (the opaque `"key"` might be used instead of `"email"` in the `POST /activate` request): + +``` +X-Zeta-Purpose: Activation +X-Zeta-Key: ... +X-Zeta-Code: 123456 +``` + +## Phone/email whitelist {#RefActivationWhitelist} + +The backend can be configured to only allow specific phone numbers or email addresses to register. The following options have to be set in `brig.yaml`: + +```yaml +optSettings: + setWhitelist: + whitelistUrl: ... # Checker URL + whitelistUser: ... # Basic auth username + whitelistPass: ... # Basic auth password +``` + +When those options are present, the backend will do a GET request at `?email=...` or `?mobile=...` for every activation request it receives. It will expect either status code 200 ("everything good") or 404 ("provided email/phone is not on the whitelist"). + +If an email address or phone number are rejected by the whitelist, `POST /activate/send` or `POST /register` will return `403 Forbidden`: + +```json +{ + "code": 403, + "label": "unauthorized", + "message": "Unauthorized e-mail address or phone number." +} +``` + +Currently emails at `@wire.com` are always considered whitelisted, regardless of the whitelist service's response. diff --git a/docs/legacy/reference/user/connection-transitions.png b/docs/legacy/reference/user/connection-transitions.png new file mode 100644 index 00000000000..1df6b8e6813 Binary files /dev/null and b/docs/legacy/reference/user/connection-transitions.png differ diff --git a/docs/legacy/reference/user/connection-transitions.xml b/docs/legacy/reference/user/connection-transitions.xml new file mode 100644 index 00000000000..1a3424e074a --- /dev/null +++ b/docs/legacy/reference/user/connection-transitions.xml @@ -0,0 +1 @@ +7Vxbc6M6Ev41qZp9cAqMAfsxzuXsPpyqqc3DmX0kINvUEPBgnGT2158WdAOSwOaiOCS7dvmCJITU+vqqhivr9vntj9Tb7/5MAhZdzY3g7cq6u5rPV/YCvnnB76LAceyiYJuGQVFkVgWP4X8ZFhpYegwDdhAaZkkSZeFeLPSTOGZ+JpRtkki8xN7bUvdVwaPvRWrpX2GQ7YrS5dypyv/Jwu2OLmM6q6LmkP2mPgK28Y5RNsuLoI5XP3vUVz4r6x4IliYJdMP/Pb/dsogTjQhSTP2hpbYcZMpiHMjpE5DgL150xDHe+D7bZyxQBg8nAmHhYP26CzP2uPd8XvMKSwtlmyTOcIVMXNb6KHBgLyzNGC5/XoSj+oMlzyxLf0MTrLWWuMiIDRtH+lqjPbbY1chOZR4iYFt2XBEB/iAdmmmCK1qjyb+2cZKOIgku9iiS2IZIEnOl0sRpoAmVjaGJq9DkkU/kYzFiShiZLy4HkqVCkHWU+D8nxzeXpAkKvBpNvrM4COPtB9PEkRjnkjQhXVWXsAo5vDRNXjtPnAWCplKnXZuWSVOvz6ssTFnkZeGLqOGaZovX+J6EnOlL/qPJEV2XOGjq4pAcU5/hWXUtJHe0OtNR5qVblikd5cQvJ95tPQg9/dfDWgfeYQccDkdw4B/Tl/IgTY5xkB8ZcHTI0uRnaSbw+k0YRbdJlKT5BawHg7+nv8KuLS8Mck7fFXbcMx1pXGFLWeErd12aNW5eICy3sJKi1qa1rK/dgwGv5lWGc2stb6klewuzH9iG//8P4gTWPf3NK8oDXpO3ioObAoR3yZ7FRckDgAjrW6FTLIigtAvSCvbedESIjAtjIMBkvUe20Rl8AZE9Pl9qtucNDu3jnS9Q8ZMNhi5L67DI+KL2aMdW8C5GMBjsKChFcQanGQAR/sM/AP/cWuPQNwCcc6MnDzTj1Li2a9iujgjdgrRU2IizRgsbfRVwwxxFcNMIe4Nb0o8mGTC60S0pdNM9je7SQhKZThu4Ve+0O265vq6waS6cGjoLCduIaWz1naUhjJRxsLbr6Toecax1PKKo+Hp4XGBUoeyIVkozHq2FhMdz0lZqTxPUBkg1NADnQKH15nG5VTMzChlMLiE/KEVwL8nbKl0r/BZVdetBskPuFihqZStUtGRqNffrVuGsxTJBzhgivHGpjWtjJb7xMpdipoVhXjvuqnyRy0JQHWop835d03bnS9ee25a7REVHkLacxmrNnKdwONplXSUCxaYaOa86m4aTbDYHAMJIw18NUiEaNXJbqToqC6gT4jE2MmVzZWEvr8HznrvmfOEunNKR6oi8zgA/fRnTtRqrNQPckVXLGVPHlhwVU2SI8apFDSgOtHUugFxUg5NBri15WcMNG3mV38nQdmhDoOSt0+hTIphocOlCH0lvveirDBMVf13Rh1Cbsln9PyU3F/L+gohEtb3MmiLSxyNXjecOQ64QmxOR2xD1mH7ottz2I8KvkDBjI2tKR7qcPTnWjNdpHZccOhTbj0cWTvNyMhHqBirkyUUa5J0hORzbFXy21JElx4c1gU++Du1PtIo1ZcdKs1hTNzH0Rw4E9HX23Rs08tQ8GdB+gspFjqG1cvQoZNklINWmHZtWvyiYYsSK7cdjU91z6OFnn9a5n2DTW6JuaeD0hQ/k85zuSFdQX4IPXUcbHNSg6E0UJa887ml8i71nFvyDJ+FFYZ4xZGSpFx/CLEziawU1kNABy7X2onALkcU7H87gIfg1z/UIIRfvBiuewyDg57TjpUfGjGRILygycCY7hJqNyQ5pyKuCIPKPXmHi0wylhHZvbykYLG1WF+Wfjv9s2dEZyn9KR7r4T/Ld6Tqt45L4VWo/nl/VSFPFrw1cWiTQAo68MM7b5EUpAw6DDC6OUS87wvyNH5NgZ9gEEtl5hctaA2i5K6+dn9UUuK+T7eWKcrIMuvVnvDMd6csFIlZ6D1Ollt7w+YyWobtTitHSMY9rrNCk63QVmlL70ULTakgbPGYJJO+HPkcTAwjxxBpBbEqCUqhTYfghto8lxqEXdAdGHZhEWt3Ckq5do+mfXnz0eJvcLeWzw2X+BrvnQU7hZFPd0VGQMmW/juyQgZE5BYra4n6tbaFKuIA1aalBg//ntp6QiaKh4BgD4wBzuo+praMWmfhO2+Bk/7SIKoWz/s38cI9W39SZyxGzL+0lHl+CuVQ3F1y18gYHnuFDtwn1TfKZZLrOWT7FNZiR7Xqx2LK8HyHfptGZb+UdErmjd8pio+u0bmzIm71i+4vk0lhqYOLWi31oNLk7nEpn7gJ381gob+qTBz55xMMkzXbJNom96L4qlfTnLnsm3msO0Z9ITsXNo9McLYia9rsXhiZZEzLOyod+bN7bQZASzkBmajX4Kc9f2P1zIq7ygvAF/m75Xyo67D2uLCtYOL+O/BbetV9QHswwI90+fctvY4WBcm0BP/SPx0fz3owIYiwzQi0/y7zm8VXqTr4u563G6/KK2SHnuryX+f5N7eWeugFiFD2JvUNxMTGluIEE44byYYR010KS7pmpf+w6F2WlyTFk7S4/AZ4gfWpET+DUKUNvmlE75nDstdmAoNtsinh2VTQUm+Xd3/n9NKUW5FbeuSUQxtxsQNf0QcpgKN5T3iAncx/zOvKeWLT2/J/bXN/URP4mf4maw4+8wwH8gfPmIKk89ApweOWDBXoobp6yLW3F0n0DI03CmZR7LcWk9FhE6uMczqv+E350kx3QZgU0pG52WEvBox+aWTIRdS/vtdOmXde9ee3mQePGnSgcPim/Iwh08Ls5X4mcOUNhMpbfKQWh9EGllDQtHE9uWy+O72feKzmxAs92WL8+t1UKwmhoqs9U5IEU77dcvcl/cLvgF+VvQvV4/p4Bg7u2qNBntIk+lMP1cvAQnd3TXW8MyJVc2MgsZ/mOmExJ8RzBZl1DHLTBp5lmnXMQ2yMWgvzqFtfUQfi86BKEb3huxSA/p/RsK8elHqZ+XyerOPcze0k9xaOIqYJ7TorMBZnZpcjEfkYaReLWSNmrVoFqE8+8nxNUT7QYbQQ1uEA6REK+gJcQCQ23INSe5fF5DJCB3HTmsV4mRBQseLIQvrAD8vjoGh/sVsBh9TjHonn1MEzr/m8= \ No newline at end of file diff --git a/docs/legacy/reference/user/connection.md b/docs/legacy/reference/user/connection.md new file mode 100644 index 00000000000..910c7452881 --- /dev/null +++ b/docs/legacy/reference/user/connection.md @@ -0,0 +1,107 @@ +# Connection {#RefConnection} + +Two users can be _connected_ or not. If the users are connected, each of them can: + +* Add the other user to a conversation (which is also a requirement for having a 1-1 conversation or doing a call). + +* See the locale of the other user. + +By default users with personal accounts are not connected. A user can send another user a _connection request_, which can be ignored or accepted by the other user. A user can also block an existing connection. + +Members of the same team are always considered connected, see [Connections between team members](#RefConnectionTeam). + +Internally, connection status is a _directed_ edge from one user to another that is attributed with a relation state and some meta information. If a user has a connection to another user, it can be in one of the six [connection states](#RefConnectionStates). + +## Connection states {#RefConnectionStates} + +### Sent {#RefConnectionSent} + +In order for two users to become connected, one of them performs a _connection request_ and the other one accepts it. Initiating a new connection results in a pending 1-1 conversation to be created with the sender as the sole member. When the connection is accepted, the other user joins the conversation. + +The creator of a new connection (i.e. the sender of the connection request) ends up in this state. From the point of view of the creator, it indicates that a connection request has been sent but not accepted (it might be blocked or ignored). + +### Pending {#RefConnectionPending} + +The recipient of a connection request automatically ends up in this state. +From his point of view, the state indicates that the connection is pending +and awaiting further action (i.e. through accepting, ignoring or blocking it). + +### Blocked {#RefConnectionBlocked} + +When a connection is in this state it indicates that the user does not want to be bothered by the other user, e.g. by receiving messages, calls or being added to conversations. + +Blocking a user does not prevent receiving further messages of that user in existing group conversations where the blocked user is a member. + +When user A blocks user B, the connection restrictions apply to both users -- e.g. A can not add B to conversations, even though it's A who blocked B and not vice-versa. + +### Ignored {#RefConnectionIgnored} + +The recipient of a connection request may decide to explicitly "ignore" the request In this state the sender can continue to send further connection attempts. The recipient can change their mind and accept the request later. + +### Cancelled {#RefConnectionCancelled} + +This is a state that the sender can change to if the connection has not yet been accepted. The state will also change for the recipient, unless blocked. + +### Accepted {#RefConnectionAccepted} + +A connection in this state is fully accepted by a user. The user thus allows the user at the other end of the connection to add him to conversations. + +For two users to be considered "connected", both A->B and B->A connections have to be in the "Accepted" state. + +## Transitions between connection states {#RefConnectionTransitions} + +![Connection state transitions](connection-transitions.png) + +(To edit this diagram, open [connection-transitions.xml](connection-transitions.xml) with .) + +## Connections between team members {#RefConnectionTeam} + +Users belonging to the same team are always implicitly treated as connected, to make it easier for team members to see each other's profiles, create conversations, etc. + +Since there is no explicit connection state between two team members, changing the connection status (e.g. blocking a fellow team member) is impossible. + +# Connection backend internals + +In the regular case of a single backend (no federation involved), and in the easiest case of two users Alice and Adham which want to start talking, the simplified internals involving the services brig and galley and cassandra can be seen as follows: (as of 2021-08) + +![Connection backend internal flow](connections-flow-1-backend.png) + +
+(To edit this diagram, copy the code in this details block to https://swimlanes.io ) + +``` +title: Connections: (no federation) + +note: this is a simplified view of what happens internall inside the backend in the simple case for connection requests. For the full details refer to the code. + +note: Alice sends a connection request to Adham (all on backend A) + +order: Alice, Adham, brig, galley, cassandra + +Alice -> brig: POST /connections + +brig -> cassandra: write in 'connections': Alice-Adham-sent +brig -> cassandra: write in 'connections': Adham-Alice-pending +note brig, galley: when a connection request is sent, that also creates a conversation of type 'connection' containing only the sender: +brig -> galley: /i/conversations/connect +galley -> cassandra: write in conversations: ID-A-A: connection/[Alice] +brig -> Adham: Event: new connection request from Alice + +...: {fas-spinner} + +note Alice, cassandra: Adham reacts and sends a request to accept the connection request + +Adham -> brig: *PUT /connections/* +brig -> cassandra: read 'connections' for Alice-Adham +brig -> cassandra: read 'connections' for Adham-Alice +brig -> cassandra: write in 'connections': Alice-Adham-accept +brig -> cassandra: write in 'connections': Adham-Alice-accept + +note brig, galley: Accepting a connection also leads to the upgrade of the 'connect' conversation to a 'one2one' conversation and adds Adham to the member list +brig -> galley: /i/conversations/:convId/accept/v2 +galley -> cassandra: write in conversations: ID-A-A: one2one/[Alice,Adham] +brig -> Alice: Event: connection request accepted +``` +
+ +The connection / one2one conversation ID is deterministically determined using a combination of the two involved user's UUIDs, using the [addv4](https://github.com/wireapp/wire-server/blob/3b1d0c5acee58bb65d8d72e71baf68dd4c0096ae/libs/types-common/src/Data/UUID/Tagged.hs#L67-L83) function. diff --git a/docs/legacy/reference/user/connections-flow-1-backend.png b/docs/legacy/reference/user/connections-flow-1-backend.png new file mode 100644 index 00000000000..7231652c45f Binary files /dev/null and b/docs/legacy/reference/user/connections-flow-1-backend.png differ diff --git a/docs/legacy/reference/user/registration.md b/docs/legacy/reference/user/registration.md new file mode 100644 index 00000000000..fee8c310227 --- /dev/null +++ b/docs/legacy/reference/user/registration.md @@ -0,0 +1,200 @@ +# Registration {#RefRegistration} + +_Authors: Artyom Kazak, Matthias Fischmann_ + +--- + +This page describes the "normal" user registration flow. Autoprovisioning is covered separately. + +## Summary {#RefRegistrationSummary} + +The vast majority of our API is only available to Wire users. Unless a user is autoprovisioned, they have to register an account by calling the `POST /register` endpoint. + +Most users also go through [activation](activation.md) -- sharing and verifying an email address and/or phone number with Wire. This can happen either before or after registration. [Certain functionality](activation.md#RefActivationBenefits) is only available to activated users. + +## Standard registration flow {#RefRegistrationStandard} + +During the standard registration flow, the user first calls [`POST /activate/send`](activation.md#RefActivationRequest) to pre-verify their email address or phone number. Phone numbers must be in [E.164][] format. + +[E.164]: https://en.wikipedia.org/wiki/E.164 + +After receiving a six-digit activation code via email/text message, it can be submitted with the registration request via `POST /register`. If the code is correct, the account will be activated immediately. Here is a sample request and response: + +``` +POST /register + +{ + // The name is mandatory + "name": "Pink", + + // 'email', 'phone', or both have to be provided + "email": "pink@example.com", + + // The password is optional + "password": "secret", + + // 6-digit 'email_code' or 'phone_code' + "email_code": "123456" +} +``` + +``` +201 Created +Set-Cookie: zuid=... + +{ + "accent_id": 0, + "assets": [], + "email": "pink@example.com", + "id": "4e1b823a-7961-4e70-bff5-30c08555c89f", + "locale": "en", + "managed_by": "wire", + "name": "Pink", + "picture": [] +} +``` + +The response contains an access cookie that can be used to access the rest of the backend API. The cookie can be assigned a label by adding a `"label": <...>` field when calling `/register`. + +If the code is incorrect or if an incorrect code has been tried enough times, the response will be as follows: + +``` +404 Not Found + +{ + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" +} +``` + +## Registration without pre-verification {#RefRegistrationNoPreverification} + +_NOTE: This flow is currently not used by any clients. At least this was the state on 2020-05-28_ + +It is also possible to call `POST /register` without verifying the email address or phone number, in which case the account will have to be activated later by calling [`POST /activate`](activation.md#RefActivationSubmit). Sample API request and response: + +``` +POST /register + +{ + // The name is mandatory + "name": "Pink", + + // 'email', 'phone', or both have to be provided + "email": "pink@example.com", + + // The password is optional + "password": "secret" +} +``` + +``` +201 Created +Set-Cookie: zuid=... + +{ + "accent_id": 0, + "assets": [], + "email": "pink@example.com", + "id": "c193136a-55fb-4534-ad72-d02a72bb16af", + "locale": "en", + "managed_by": "wire", + "name": "Pink", + "picture": [] +} +``` + +A verification email will be sent to the email address (if provided), and a verification text message will be sent to the phone number (also, if provided). + +## Anonymous registration, aka "Wireless" {#RefRegistrationWireless} + +A user can be created without either email or phone number, in which case only `"name"` is required. The `"name"` does not have to be unique. This feature is used for [guest rooms](https://wire.com/en/features/encrypted-guest-rooms/). + +An anonymous, non-activated account is only usable for a period of time specified in `brig.yaml` at `zauth.authSettings.sessionTokenTimeout`, which is set to 1 day for Wire production. (The access cookie returned by `/register` can not be refreshed, and an anonymous user can not use `/login` to get a new cookie.) + +Sample API request and response: + +``` +POST /register + +{ + "name": "Pink" +} +``` + +``` +201 Created +Set-Cookie: zuid=... + +{ + "accent_id": 0, + "assets": [], + "expires_at": "2019-04-18T14:09:43.732Z", + "id": "1914b4e6-eb72-4943-8925-06314b24ed68", + "locale": "en", + "managed_by": "wire", + "name": "Pink" + "picture": [], +} +``` + +## Blocking creation of personal users, new teams {#RefRestrictRegistration} + +[moved here](https://docs.wire.com/how-to/install/configuration-options.html#blocking-creation-of-personal-users-new-teams) + +### Details + +You can find the exhaustive list of all routes here: + +https://github.com/wireapp/wire-server-deploy/blob/de7e3e8c709f8baaae66b1540a1778871044f170/charts/nginz/values.yaml#L35-L371 + +The paths not cryptographically authenticated can be found by searching for the `disable_zauth:` flag (must be true for `env: prod` or `env: all`). + +Two of them allow users to create new users or teams: + +- `/register` +- `/activate` + +These end-points support 5 flows: + +1. new team account +2. new personal (teamless) account +3. invitation code from team, new member +4. ephemeral user +5. [not supported by clients] new *inactive* user account + +We need an option to block 1, 2, 5 on-prem; 3, 4 should remain available (no block option). There are also provisioning flows via SAML or SCIM, which are not critical. In short, this could refactored into: + + * Allow team members to register (via email/phone or SSO) + * Allow ephemeral users + +During registration, we can take advantage of [NewUserOrigin](https://github.com/wireapp/wire-server/blob/a89b9cd818997e7837e5d0938ecfd90cf8dd9e52/libs/wire-api/src/Wire/API/User.hs#L625); we're particularly interested in `NewUserOriginTeamUser` --> only `NewTeamMember` or `NewTeamMemberSSO` should be accepted. In case this is a `Nothing`, we need to check if the user expires, i.e., if the user has no identity (and thus `Ephemeral`). + +So `/register` should only succeed iff at least one of these conditions is true: + +``` +import Brig.Types.User +isNewUserTeamMember || isNewUserEphemeral +``` + +The rest of the unauthorized end-points is safe: + +- `/password-reset` +- `/delete`: similar to password reset, for deleting a personal account with password. +- `/login` +- `/login/send` +- `/access` +- `/sso/initiate-login`: authenticated via IdP. +- `/sso/finalize-login`: authenticated via IdP. +- `/sso`: authenticated via IdP or ok to expose to world (`/metadata`) +- `/scim/v2`: authenticated via HTTP simple auth. +- `~* ^/teams/invitations/info$`: only `GET`; requires invitation code. +- `~* ^/teams/invitations/by-email$`: only `HEAD`. +- `/invitations/info`: discontinued feature, can be removed from nginz config. +- `/conversations/code-check`: link validatoin for ephemeral/guest users. +- `/provider/*`: bots need to be registered to a team before becoming active. so if an attacker does not get access to a team, they cannot deploy a bot. +- `~* ^/custom-backend/by-domain/([^/]*)$`: only `GET`; only exposes a list of domains that has is maintained through an internal end-point. used to redirect stock clients from the cloud instance to on-prem instances. +- `~* ^/teams/api-docs`: only `GET`; swagger for part of the rest API. safe: it is trivial to identify the software that is running on the instance, and from there it is trivial to get to the source on github, where this can be obtained easily, and more. +- `/billing`: separate billing service, usually not installed for on-prem instances. +- `/calling-test`: separate testing service that has its own authentication. diff --git a/docs/legacy/reference/user/rich-info.md b/docs/legacy/reference/user/rich-info.md new file mode 100644 index 00000000000..e6ecb60c297 --- /dev/null +++ b/docs/legacy/reference/user/rich-info.md @@ -0,0 +1,148 @@ +# Rich info {#RefRichInfo} + +_Author: Artyom Kazak_ + +--- + +This page describes a part of the user profile called "Rich info". The corresponding feature is called "Rich profiles". + +## Summary {#RefRichInfoSummary} + +For every team user we can store a list of key-value pairs that are displayed in the user profile. This is similar to "custom profile fields" in Slack and other enterprise messengers. + +Different users can have different sets of fields; there is no team-wide schema for fields. All field values are strings. Fields are passed as an ordered list, and the order information is preserved when displaying fields in client apps. + +Only team members and partners can see the user's rich info. + +## API {#RefRichInfoApi} + +### Querying rich info {#RefRichInfoGet} + +`GET /users/:uid/rich-info`. Sample output: + +```json +{ + "version": 0, + "fields": [ + { + "type": "Department", + "value": "Sales & Marketing" + }, + { + "type": "Favorite color", + "value": "Blue" + } + ] +} +``` + +If the requesting user is not allowed to see rich info, error code 403 is returned with the `"insufficient-permissions"` error label. + +Otherwise, if the rich info is missing, an empty field list is returned: + +```json +{ + "version": 0, + "fields": [] +} +``` + +### Setting rich info {#RefRichInfoPut} + +**Not implemented yet.** Currently the only way to set rich info is via SCIM. + +### Events {#RefRichInfoEvents} + +**Not implemented yet.** + +When user's rich info changes, the backend sends out an event to all team members: + +```json +{ + "type": "user.rich-info-update", + "user": { + "id": "" + } +} +``` + +Connected users who are not members of user's team will not receive an event (nor can they query user's rich info by other means). + +## SCIM support {#RefRichInfoScim} + +Rich info can be pushed to Wire by setting JSON keys under the `"urn:ietf:params:scim:schemas:extension:wire:1.0:User"` extension. Both `PUT /scim/v2/Users/:id` , `PATCH /scim/v2/Users/:id` and `POST /scim/v2/Users/:id` can contain rich info. Here is an example for `PUT`: + +```javascript +PUT /scim/v2/Users/:id + +{ + ..., + "urn:ietf:params:scim:schemas:extension:wire:1.0:User": { + "Department": "Sales & Marketing", + "FavoriteColor": "Blue" + } +} +``` + +Here is an example for `PATCH`: + +```json +PATCH /scim/v2/Users/:id + +{ + "schemas": [ + "urn:ietf:params:scim:api:messages:2.0:PatchOp" + ], + "operations": [ + { + "op": "add", + "path": "urn:ietf:params:scim:schemas:extension:wire:1.0:User:Department", + "value": "Development " + }, + { + "op": "replace", + "path": "urn:ietf:params:scim:schemas:extension:wire:1.0:User:Country", + "value": "Germany" + }, + { + "op": "remove", + "path": "urn:ietf:params:scim:schemas:extension:wire:1.0:User:City" + } + ] +} + +``` + +Rich info set via SCIM can be queried by doing a `GET /scim/v2/Users` or `GET /scim/v2/Users/:id` query. + +### Set up SCIM RichInfo mapping in Azure {#RefRichInfoScimAgents} + +Go to your provisioning page + +![image](https://user-images.githubusercontent.com/628387/119977043-393b3000-bfb8-11eb-9e5b-18a955ca3181.png) + +Click "Edit attribute mappings" + +Then click "Mappings" And then click **Synchronize Azure Active Directory Users to _appname_** +![image](https://user-images.githubusercontent.com/628387/119977488-c9797500-bfb8-11eb-81b8-46376f5fdadb.png) + +Click "Show Advanced options" and then **Edit attribute list for _appname_** +![image](https://user-images.githubusercontent.com/628387/119977905-3f7ddc00-bfb9-11eb-90e2-28da82c6f13e.png) + +Add a new attribute name. The type should be `String` and the name should be prefixed with `urn:ietf:params:scim:schemas:extension:wire:1.0:User:` +e.g. `urn:ietf:params:scim:schemas:extension:wire:1.0:User:Location` + +![image](https://user-images.githubusercontent.com/628387/119978050-70f6a780-bfb9-11eb-8919-93e32bf76d79.png) + +Hit **Save** and afterwards hit **Add New Mapping** + +Select the Azure AD Source attribute you want to map, and map it to the custom **Target Attribute** that you just added. +![image](https://user-images.githubusercontent.com/628387/119978316-c5018c00-bfb9-11eb-9290-2076ac1a05df.png) + + + +## Limitations {#RefRichInfoLimitations} + +* The whole of user-submitted information (field names and values) cannot exceed 5000 characters in length. There are no limitations on the number of fields, or the maximum of individual field names or values. + +* Field values can not be empty (`""`). If they are empty, the corresponding field will be removed. diff --git a/docs/reference/cassandra-schema.cql b/docs/reference/cassandra-schema.cql deleted file mode 100644 index 5afd359b05e..00000000000 --- a/docs/reference/cassandra-schema.cql +++ /dev/null @@ -1,1898 +0,0 @@ --- automatically generated with `make git-add-cassandra-schema` - -CREATE KEYSPACE galley_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; - -CREATE TYPE galley_test.permissions ( - self bigint, - copy bigint -); - -CREATE TYPE galley_test.pubkey ( - typ int, - size int, - pem blob -); - -CREATE TABLE galley_test.meta ( - id int, - version int, - date timestamp, - descr text, - PRIMARY KEY (id, version) -) WITH CLUSTERING ORDER BY (version ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.conversation ( - conv uuid PRIMARY KEY, - access set, - access_role int, - access_roles_v2 set, - creator uuid, - deleted boolean, - epoch bigint, - group_id blob, - message_timer bigint, - name text, - protocol int, - receipt_mode int, - team uuid, - type int -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.user_team ( - user uuid, - team uuid, - PRIMARY KEY (user, team) -) WITH CLUSTERING ORDER BY (team ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.service ( - provider uuid, - id uuid, - auth_token ascii, - base_url blob, - enabled boolean, - fingerprints set, - PRIMARY KEY (provider, id) -) WITH CLUSTERING ORDER BY (id ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.data_migration ( - id int, - version int, - date timestamp, - descr text, - PRIMARY KEY (id, version) -) WITH CLUSTERING ORDER BY (version ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.team_features ( - team_id uuid PRIMARY KEY, - app_lock_enforce int, - app_lock_inactivity_timeout_secs int, - app_lock_status int, - conference_calling int, - digital_signatures int, - file_sharing int, - file_sharing_lock_status int, - guest_links_lock_status int, - guest_links_status int, - legalhold_status int, - search_visibility_inbound_status int, - search_visibility_status int, - self_deleting_messages_lock_status int, - self_deleting_messages_status int, - self_deleting_messages_ttl int, - snd_factor_password_challenge_lock_status int, - snd_factor_password_challenge_status int, - sso_status int, - validate_saml_emails int -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.member ( - conv uuid, - user uuid, - conversation_role text, - hidden boolean, - hidden_ref text, - mls_clients set, - otr_archived boolean, - otr_archived_ref text, - otr_muted boolean, - otr_muted_ref text, - otr_muted_status int, - provider uuid, - service uuid, - status int, - PRIMARY KEY (conv, user) -) WITH CLUSTERING ORDER BY (user ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.custom_backend ( - domain text PRIMARY KEY, - config_json_url blob, - webapp_welcome_url blob -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.user_remote_conv ( - user uuid, - conv_remote_domain text, - conv_remote_id uuid, - hidden boolean, - hidden_ref text, - otr_archived boolean, - otr_archived_ref text, - otr_muted_ref text, - otr_muted_status int, - PRIMARY KEY (user, conv_remote_domain, conv_remote_id) -) WITH CLUSTERING ORDER BY (conv_remote_domain ASC, conv_remote_id ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.legalhold_whitelisted ( - team uuid PRIMARY KEY -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.member_remote_user ( - conv uuid, - user_remote_domain text, - user_remote_id uuid, - conversation_role text, - PRIMARY KEY (conv, user_remote_domain, user_remote_id) -) WITH CLUSTERING ORDER BY (user_remote_domain ASC, user_remote_id ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.team_member ( - team uuid, - user uuid, - invited_at timestamp, - invited_by uuid, - legalhold_status int, - perms frozen, - PRIMARY KEY (team, user) -) WITH CLUSTERING ORDER BY (user ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.team_notifications ( - team uuid, - id timeuuid, - payload blob, - PRIMARY KEY (team, id) -) WITH CLUSTERING ORDER BY (id ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.legalhold_pending_prekeys ( - user uuid, - key int, - data text, - PRIMARY KEY (user, key) -) WITH CLUSTERING ORDER BY (key ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.group_id_conv_id ( - group_id blob PRIMARY KEY, - conv_id uuid, - domain text -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.user ( - user uuid, - conv uuid, - PRIMARY KEY (user, conv) -) WITH CLUSTERING ORDER BY (conv ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.legalhold_service ( - team_id uuid PRIMARY KEY, - auth_token ascii, - base_url blob, - fingerprint blob, - pubkey pubkey -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.conversation_codes ( - key ascii, - scope int, - conversation uuid, - value ascii, - PRIMARY KEY (key, scope) -) WITH CLUSTERING ORDER BY (scope ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.clients ( - user uuid PRIMARY KEY, - clients set -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.team_conv ( - team uuid, - conv uuid, - managed boolean, - PRIMARY KEY (team, conv) -) WITH CLUSTERING ORDER BY (conv ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.team ( - team uuid PRIMARY KEY, - binding boolean, - creator uuid, - deleted boolean, - icon text, - icon_key text, - name text, - search_visibility int, - status int -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.billing_team_member ( - team uuid, - user uuid, - PRIMARY KEY (team, user) -) WITH CLUSTERING ORDER BY (user ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE KEYSPACE gundeck_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; - -CREATE TABLE gundeck_test.push ( - ptoken text, - app text, - transport int, - client text, - connection blob, - usr uuid, - PRIMARY KEY (ptoken, app, transport) -) WITH CLUSTERING ORDER BY (app ASC, transport ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE gundeck_test.notifications ( - user uuid, - id timeuuid, - clients set, - payload blob, - PRIMARY KEY (user, id) -) WITH CLUSTERING ORDER BY (id ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'tombstone_threshold': '0.1'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE gundeck_test.meta ( - id int, - version int, - date timestamp, - descr text, - PRIMARY KEY (id, version) -) WITH CLUSTERING ORDER BY (version ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE gundeck_test.user_push ( - usr uuid, - ptoken text, - app text, - transport int, - arn text, - client text, - connection blob, - PRIMARY KEY (usr, ptoken, app, transport) -) WITH CLUSTERING ORDER BY (ptoken ASC, app ASC, transport ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE KEYSPACE brig_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; - -CREATE TYPE brig_test.asset ( - typ int, - key text, - size int -); - -CREATE TYPE brig_test.pubkey ( - typ int, - size int, - pem blob -); - -CREATE TABLE brig_test.team_invitation_info ( - code ascii PRIMARY KEY, - id uuid, - team uuid -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.rich_info ( - user uuid PRIMARY KEY, - json blob -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.user_keys_hash ( - key blob PRIMARY KEY, - key_type int, - user uuid -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.service_tag ( - bucket int, - tag bigint, - name text, - service uuid, - provider uuid, - PRIMARY KEY ((bucket, tag), name, service) -) WITH CLUSTERING ORDER BY (name ASC, service ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.login_codes ( - user uuid PRIMARY KEY, - code text, - retries int, - timeout timestamp -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.unique_claims ( - value text PRIMARY KEY, - claims set -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.user_cookies ( - user uuid, - expires timestamp, - id bigint, - created timestamp, - label text, - succ_id bigint, - type int, - PRIMARY KEY (user, expires, id) -) WITH CLUSTERING ORDER BY (expires ASC, id ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.mls_key_packages ( - user uuid, - client text, - ref blob, - data blob, - PRIMARY KEY ((user, client), ref) -) WITH CLUSTERING ORDER BY (ref ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.mls_key_package_refs ( - ref blob PRIMARY KEY, - client text, - conv uuid, - conv_domain text, - domain text, - user uuid -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.excluded_phones ( - prefix text PRIMARY KEY, - comment text -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.codes ( - user uuid, - scope int, - code text, - retries int, - PRIMARY KEY (user, scope) -) WITH CLUSTERING ORDER BY (scope ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.user_handle ( - handle text PRIMARY KEY, - user uuid -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.service ( - provider uuid, - id uuid, - assets list>, - auth_tokens list, - base_url blob, - descr text, - enabled boolean, - fingerprints list, - name text, - pubkeys list>, - summary text, - tags set, - PRIMARY KEY (provider, id) -) WITH CLUSTERING ORDER BY (id ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.team_invitation_email ( - email text, - team uuid, - code ascii, - invitation uuid, - PRIMARY KEY (email, team) -) WITH CLUSTERING ORDER BY (team ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.invitation_info ( - code ascii PRIMARY KEY, - id uuid, - inviter uuid -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.service_whitelist ( - team uuid, - provider uuid, - service uuid, - PRIMARY KEY (team, provider, service) -) WITH CLUSTERING ORDER BY (provider ASC, service ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.provider ( - id uuid PRIMARY KEY, - descr text, - email text, - name text, - password blob, - url blob -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.user_keys ( - key text PRIMARY KEY, - user uuid -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.mls_public_keys ( - user uuid, - client text, - sig_scheme text, - key blob, - PRIMARY KEY (user, client, sig_scheme) -) WITH CLUSTERING ORDER BY (client ASC, sig_scheme ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.invitee_info ( - invitee uuid PRIMARY KEY, - conv uuid, - inviter uuid -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.provider_keys ( - key text PRIMARY KEY, - provider uuid -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.service_team ( - provider uuid, - service uuid, - team uuid, - user uuid, - conv uuid, - PRIMARY KEY ((provider, service), team, user) -) WITH CLUSTERING ORDER BY (team ASC, user ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.blacklist ( - key text PRIMARY KEY -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.service_whitelist_rev ( - provider uuid, - service uuid, - team uuid, - PRIMARY KEY ((provider, service), team) -) WITH CLUSTERING ORDER BY (team ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.team_invitation ( - team uuid, - id uuid, - code ascii, - created_at timestamp, - created_by uuid, - email text, - name text, - phone text, - role int, - PRIMARY KEY (team, id) -) WITH CLUSTERING ORDER BY (id ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.user ( - id uuid PRIMARY KEY, - accent list, - accent_id int, - activated boolean, - assets list>, - country ascii, - email text, - email_unvalidated text, - expires timestamp, - feature_conference_calling int, - handle text, - language ascii, - managed_by int, - name text, - password blob, - phone text, - picture list, - provider uuid, - searchable boolean, - service uuid, - sso_id text, - status int, - team uuid -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.properties ( - user uuid, - key ascii, - value blob, - PRIMARY KEY (user, key) -) WITH CLUSTERING ORDER BY (key ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.service_user ( - provider uuid, - service uuid, - user uuid, - conv uuid, - team uuid, - PRIMARY KEY ((provider, service), user) -) WITH CLUSTERING ORDER BY (user ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.prekeys ( - user uuid, - client text, - key int, - data text, - PRIMARY KEY (user, client, key) -) WITH CLUSTERING ORDER BY (client ASC, key ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.password_reset ( - key ascii PRIMARY KEY, - code ascii, - retries int, - timeout timestamp, - user uuid -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.clients ( - user uuid, - client text, - capabilities set, - class int, - cookie text, - ip inet, - label text, - lat double, - lon double, - model text, - tstamp timestamp, - type int, - PRIMARY KEY (user, client) -) WITH CLUSTERING ORDER BY (client ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.budget ( - key text PRIMARY KEY, - budget int -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.connection_remote ( - left uuid, - right_domain text, - right_user uuid, - conv_domain text, - conv_id uuid, - last_update timestamp, - status int, - PRIMARY KEY (left, right_domain, right_user) -) WITH CLUSTERING ORDER BY (right_domain ASC, right_user ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.users_pending_activation ( - user uuid PRIMARY KEY, - expires_at timestamp -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.connection ( - left uuid, - right uuid, - conv uuid, - last_update timestamp, - message text, - status int, - PRIMARY KEY (left, right) -) WITH CLUSTERING ORDER BY (right ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; -CREATE INDEX conn_status ON brig_test.connection (status); - -CREATE TABLE brig_test.meta ( - id int, - version int, - date timestamp, - descr text, - PRIMARY KEY (id, version) -) WITH CLUSTERING ORDER BY (version ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.invitation ( - inviter uuid, - id uuid, - code ascii, - created_at timestamp, - email text, - name text, - phone text, - PRIMARY KEY (inviter, id) -) WITH CLUSTERING ORDER BY (id ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.activation_keys ( - key ascii PRIMARY KEY, - challenge ascii, - code ascii, - key_text text, - key_type ascii, - retries int, - user uuid -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.vcodes ( - key ascii, - scope int, - account uuid, - email text, - phone text, - retries int, - value ascii, - PRIMARY KEY (key, scope) -) WITH CLUSTERING ORDER BY (scope ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.service_prefix ( - prefix text, - name text, - service uuid, - provider uuid, - PRIMARY KEY (prefix, name, service) -) WITH CLUSTERING ORDER BY (name ASC, service ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE KEYSPACE spar_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; - -CREATE TABLE spar_test.scim_external_ids ( - external text PRIMARY KEY, - user uuid -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.bind_cookie ( - cookie text PRIMARY KEY, - session_owner uuid -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.user_v2 ( - issuer text, - normalized_uname_id text, - sso_id text, - uid uuid, - PRIMARY KEY (issuer, normalized_uname_id) -) WITH CLUSTERING ORDER BY (normalized_uname_id ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.data_migration ( - id int, - version int, - date timestamp, - descr text, - PRIMARY KEY (id, version) -) WITH CLUSTERING ORDER BY (version ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.authresp ( - resp text PRIMARY KEY, - end_of_life timestamp -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.idp_raw_metadata ( - id uuid PRIMARY KEY, - metadata text -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.issuer_idp ( - issuer text PRIMARY KEY, - idp uuid -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.idp ( - idp uuid PRIMARY KEY, - api_version int, - extra_public_keys list, - issuer text, - old_issuers list, - public_key blob, - replaced_by uuid, - request_uri text, - team uuid -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.default_idp ( - partition_key_always_default text, - idp uuid, - PRIMARY KEY (partition_key_always_default, idp) -) WITH CLUSTERING ORDER BY (idp ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.team_provisioning_by_team ( - team uuid, - id uuid, - created_at timestamp, - descr text, - idp uuid, - token_ text, - PRIMARY KEY (team, id) -) WITH CLUSTERING ORDER BY (id ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.meta ( - id int, - version int, - date timestamp, - descr text, - PRIMARY KEY (id, version) -) WITH CLUSTERING ORDER BY (version ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.verdict ( - req text PRIMARY KEY, - format_con int, - format_mobile_error text, - format_mobile_success text -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.authreq ( - req text PRIMARY KEY, - end_of_life timestamp -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.team_provisioning_by_token ( - token_ text PRIMARY KEY, - created_at timestamp, - descr text, - id uuid, - idp uuid, - team uuid -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.team_idp ( - team uuid, - idp uuid, - PRIMARY KEY (team, idp) -) WITH CLUSTERING ORDER BY (idp ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.issuer_idp_v2 ( - issuer text, - team uuid, - idp uuid, - PRIMARY KEY (issuer, team) -) WITH CLUSTERING ORDER BY (team ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.scim_user_times ( - uid uuid PRIMARY KEY, - created_at timestamp, - last_updated_at timestamp -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.scim_external ( - team uuid, - external_id text, - user uuid, - PRIMARY KEY (team, external_id) -) WITH CLUSTERING ORDER BY (external_id ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.user ( - issuer text, - sso_id text, - uid uuid, - PRIMARY KEY (issuer, sso_id) -) WITH CLUSTERING ORDER BY (sso_id ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - diff --git a/docs/reference/cassandra-schema.cql b/docs/reference/cassandra-schema.cql new file mode 120000 index 00000000000..77e9ef36a84 --- /dev/null +++ b/docs/reference/cassandra-schema.cql @@ -0,0 +1 @@ +../../cassandra-schema.cql \ No newline at end of file diff --git a/docs/reference/config-options.md b/docs/reference/config-options.md index 2b30e07ac85..2a6e342a549 100644 --- a/docs/reference/config-options.md +++ b/docs/reference/config-options.md @@ -1,493 +1 @@ -# Config Options {#RefConfigOptions} - -Fragment. - -This page is about the yaml files that determine the configuration of -the Wire backend services. - -## Settings in galley - -``` -# [galley.yaml] -settings: - enableIndexedBillingTeamMembers: false -``` - -### Indexed Billing Team Members - -Use indexed billing team members for journaling. When `enabled`, -galley would use the `billing_team_member` table to send billing -events with user ids of team owners (who have the `SetBilling` -permission). Before enabling this flag, the `billing_team_member` -table must be backfilled. - -Even when the flag is `disabled`, galley will keep writing to the -`biling_team_member` table, this flag only affects the reads and has -been added in order to deploy new code and backfill data in -production. - -## Feature flags - -Feature flags can be used to turn features on or off, or determine the -behavior of the features. Example: - -``` -# [galley.yaml] -settings: - featureFlags: - sso: disabled-by-default - legalhold: disabled-by-default - teamSearchVisibility: disabled-by-default - setEmailVisibility: visible_to_self -``` - -The `featureFlags` field in the galley settings is mandatory, and all -features must be listed. Each feature defines its own set of allowed -flag values. (The reason for that is that as we will see, the -semantics is slightly different (or more specific) than boolean.) - -### SSO - -This sets the default setting for all teams, and can be overridden by -customer support / backoffice. [Allowed -values](https://github.com/wireapp/wire-server/blob/46713382a1a6544de3936eb03e987b9f76df3faa/libs/galley-types/src/Galley/Types/Teams.hs#L327-L329): -`disabled-by-default`, `enabled-by-default`. - -IMPORTANT: if you change this from 'enabled-by-default' to -'disabled-by-default' in production, you need to run [this migration -script](https://github.com/wireapp/wire-server/tree/master/tools/db/migrate-sso-feature-flag) -to fix all teams that have registered an idp. (if you don't, the idp -will keep working, but the admin won't be able to register new idps.) - -### LegalHold - -Optionally block customer support / backoffice from enabling legal -hold for individual teams. [Allowed -values](https://github.com/wireapp/wire-server/blob/46713382a1a6544de3936eb03e987b9f76df3faa/libs/galley-types/src/Galley/Types/Teams.hs#L332-L334): -'disabled-permanently', 'disabled-by-default'. - -IMPORTANT: If you switch this back to `disabled-permanently` from -`disabled-by-default`, LegalHold devices may still be active in teams -that have created them while it was allowed. This may change in the -future. - -### Team Feature teamSearchVisibility - -The feature flag `teamSearchVisibility` affects the outbound search of user -searches. If it is set to `no-name-outside-team` for a team then all users of -that team will no longer be able to find users that are not part of their team -when searching. This also includes finding other users by by providing their -exact handle. By default it is set to `standard`, which doesn't put any -additional restrictions to outbound searches. - -The setting can be changed via endpoint: - -``` -GET /teams/{tid}/search-visibility - -- Shows the current TeamSearchVisibility value for the given team - -PUT /teams/{tid}/search-visibility - -- Set specific search visibility for the team - -pull-down-menu "body": - "standard" - "no-name-outside-team" -``` - -The default setting that applies to all teams on the instance can be defined at configuration - -```yaml -settings: - featureFlags: - teamSearchVisibility: disabled-by-default # or enabled-by-default -``` - -where disabled is equivalent to `standard` and enabled is equivalent to `no-name-outside-team`. Individual teams may ovewrite the default setting. - -On wire cloud the default setting is `standard`. - -### TeamFeature searchVisibilityInbound - -The team feature flag `searchVisibilityInbound` affects if the team's users are -searchable by users from _other_ teams. The default setting is -`searchable-by-own-team` which hides users from search results by users from -other teams. If it is set to `searchable-by-all-teams` then users of this team -may be included in the results of search queries by other users. - -Note: The configuration of this flag does _not_ affect search results when the -search query matches the handle exactly. If the handle is provdided then any user on the instance can find users. - -This team feature flag can only by toggled by site-administrators with direct access to the galley instance: - -``` -PUT /i/teams/{tid}/features/search-visibility-inbound -with JSON body {"status": "enabled"} or body {"status": disabled} -``` - -where `enabled` is equivalent to `searchable-by-all-teams` and disabled is equivalent to `searchable-by-own-team`. - -The default setting that applies to all teams on the instance can be defined at configuration. - -```yaml -searchVisibilityInbound: - defaults: - status: enabled # OR disabled -``` - -Individual teams can overwrite the default setting. - -### Email Visibility - -[Allowd values](https://github.com/wireapp/wire-server/blob/0126651a25aabc0c5589edc2b1988bb06550a03a/services/brig/src/Brig/Options.hs#L304-L306) and their [description](https://github.com/wireapp/wire-server/blob/0126651a25aabc0c5589edc2b1988bb06550a03a/services/brig/src/Brig/Options.hs#L290-L299). - -### Classified domains - -To enable classified domains, the following needs to be in galley.yaml or wire-server/values.yaml under `settings` / `featureFlags`: - -```yaml -classifiedDomains: - status: enabled - config: - domains: ["example.com", "example2.com"] -``` - -Note that when enabling this feature, it is important to provide your own domain -too in the list of domains. In the example above, `example.com` or `example2.com` is your domain. - -To disable, either omit the entry entirely (it is disabled by default), or provide the following: - -```yaml -classifiedDomains: - status: disabled - config: - domains: [] -``` - -### Conference Calling - -The `conferenceCalling` feature flag controls whether a user can initiate a conference call. The flag can be toggled between its states `enabled` and `disabled` per team via an internal endpoint. - -The `conferenceCalling` section in `featureFlags` defines the state of the `conferenceCalling` feature flag for all personal users (users that don't belong to a team). For personal users there is no way to toggle the flag, so the setting of the config section wholly defines the state of `conferenceCalling` flag for all personal users. - -The `conferenceCalling` section in `featureFlags` also defines the _initial_ state of the `conferenceCalling` flag for all teams. After the flag is set for the first time for a team via the internal endpoint the value from the config section will be ignored. - -Example value for the config section: -```yaml -conferenceCalling: - defaults: - status: enabled -``` - -The `conferenceCalling` section is optional in `featureFlags`. If it is omitted then it is assumed to be `enabled`. - -See also: conference falling for personal accounts (below). - -### File Sharing - -File sharing is enabled and unlocked by default. If you want a different configuration, use the following syntax: - -```yaml -fileSharing: - defaults: - status: disabled|enabled - lockStatus: locked|unlocked -``` - -These are all the possible combinations of `status` and `lockStatus`: - -| `status` | `lockStatus` | | -| ---------- | ------------ | ------------------------------------------------- | -| `enabled` | `locked` | Feature enabled, cannot be disabled by team admin | -| `enabled` | `unlocked` | Feature enabled, can be disabled by team admin | -| `disabled` | `locked` | Feature disabled, cannot be enabled by team admin | -| `disabled` | `unlocked` | Feature disabled, can be enabled by team admin | - -The lock status for individual teams can be changed via the internal API (`PUT /i/teams/:tid/features/fileSharing/(un)?locked`). - -The feature status for individual teams can be changed via the public API (if the feature is unlocked). - -### Validate SAML Emails - -If this is enabled, if a new user account is created with an email address as SAML NameID or SCIM externalId, users will receive a validation email. If they follow the validation procedure, they will be able to receive emails about their account, eg., if a new device is associated with the account. If the user does not validate their email address, they can still use it to login. - -Validate SAML emails is enabled by default; this is almost always what you want. If you want a different configuration, use the following syntax: - -```yaml -# galley.yaml -validateSAMLEmails: - defaults: - status: disabled -``` - -### 2nd Factor Password Challenge - -By default Wire enforces a 2nd factor authentication for certain user operations like e.g. activating an account, changing email or password, or deleting an account. -If this feature is enabled, a 2nd factor password challenge will be performed for a set of additional user operations like e.g. for generating SCIM tokens, login, or adding a client. - -Usually the default is what you want. If you explicitly want to enable the feature, use the following syntax: - -```yaml -# galley.yaml -sndFactorPasswordChallenge: - defaults: - status: disabled|enabled - lockStatus: locked|unlocked -``` - -### Federation Domain - -Regardless of whether a backend wants to enable federation or not, the operator -must decide what its domain is going to be. This helps in keeping things -simpler across all components of Wire and also enables to turn on federation in -the future if required. - -For production uses, it is highly recommended that this domain be configured as -something that is controlled by the operator(s). The backend or frontend do not -need to be available on this domain. As per our current federation design, you -must be able to set an SRV record for `_wire-server-federator._tcp.`. -This record should have entries which lead to the federator. - -**IMPORTANT** Once this option is set, it cannot be changed without breaking -experience for all the users which are already using the backend. - -This configuration needs to be made in brig, cargohold and galley (note the -slighly different spelling of the config options). - -```yaml -# brig.yaml -optSettings: - setFederationDomain: example.com -``` - -```yaml -# cargohold.yaml -settings: - federationDomain: example.com -``` - -```yaml -# galley.yaml -settings: - federationDomain: example.com -``` - -### Federation allow list - -As of 2021-07, federation (whatever is implemented by the time you read this) is turned off by default by means of having an empty allow list: - -```yaml -# federator.yaml -optSettings: - federationStrategy: - allowedDomains: [] -``` - -You can choose to federate with a specific list of allowed servers: - - -```yaml -# federator.yaml -optSettings: - federationStrategy: - allowedDomains: - - server1.example.com - - server2.example.com -``` - -or, you can federate with everyone: - -```yaml -# federator.yaml -optSettings: - federationStrategy: - # note the 'empty' value after 'allowAll' - allowAll: - -# when configuring helm charts, this becomes (note 'true' after 'allowAll') -# inside helm_vars/wire-server: -federator: - optSettings: - federationStrategy: - allowAll: true -``` - -### Federation TLS Config - -When a federator connects with another federator, it does so over HTTPS. There -are a few options to configure the CA for this: -1. `useSystemCAStore`: Boolean. If set to `True` it will use the system CA. -2. `remoteCAStore`: Maybe Filepath. This config option can be used to specify - multiple certificates from either a single file (multiple PEM formatted - certificates concatenated) or directory (one certificate per file, file names - are hashes from certificate). -3. `clientCertificate`: Maybe Filepath. A client certificate to use when - connecting to remote federators. If this option is omitted, no client - certificate is used. If it is provided, then the `clientPrivateKey` option - (see below) must be provided as well. -4. `clientPrivateKey`: Maybe Filepath. The private key corresponding to the - `clientCertificate` option above. It is an error to provide only a private key - without the corresponding certificate. - -Both the `useSystemCAStore` and `remoteCAStore` options can be specified, in -which case the stores are concatenated and used for verifying certificates. -When `useSystemCAStore` is set to `false` and `remoteCAStore` is not provided, -all outbound connections will fail with a TLS error as there will be no CA for -verifying the server certificate. - -#### Examples - -Federate with anyone, no client certificates, use system CA store to verify -server certificates: - -```yaml -federator: - optSettings: - federationStrategy: - allowAll: - useSystemCAStore: true -``` - -Federate only with `server2.example.com`, use a client certificate and a -specific CA: - -```yaml -federator: - optSettings: - federationStrategy: - allowedDomains: - - server2.example.com - useSystemCAStore: false - clientCertificate: client.pem - clientPrivateKey: client-key.pem -``` - -## Settings in brig - -Some features (as of the time of writing this: only -`conferenceCalling`) allow to set defaults for personal accounts in -brig. Those are taken into account in galley's end-points `GET -/feature-configs*`. - -To be specific: - -### Conference Calling - -Two values can be configured for personal accounts: a default for when -the user record contains `null` as feature config, and default that -should be inserted into the user record when creating new users: - -``` -# [brig.yaml] -settings: - setFeatureFlags: - conferenceCalling: - defaultForNew: - status: disabled - defaultForNull: - status: enabled -``` - -You can omit the entire `conferenceCalling` block, but not parts of -it. Built-in defaults: `defaultForNew: null` (user record attribute -is left empty); `defaultForNull: enabled`. This maintains behavior -prior to the introduction of this change, while allowing site owners -to postpone the decision about the default setting. - -When new users are created, their config will be initialized with -what's in `defaultForNew`. - -When a `null` value is encountered, it is assumed to be -`defaultForNull`. - -(Introduced in https://github.com/wireapp/wire-server/pull/1811.) - -### SFT configuration - -Configuring SFT load balancing can be done in two (mutually exclusive) settings: - -1) Configuring a SRV DNS record based load balancing setting - -``` -# [brig.yaml] -sft: - sftBaseDomain: sft.wire.example.com - sftSRVServiceName: sft - sftDiscoveryIntervalSeconds: 10 - sftListLength: 20 -``` - -or - -2) Configuring a HTTP-based load balancing setting - -``` -# [brig.yaml] -settings: - setSftStaticUrl: https://sft.wire.example.com -``` - -This setting assumes that the sft load balancer has been deployed with the `sftd` helm chart. - -Additionally if `setSftListAllServers` is set to `enabled` (disabled by default) then the `/calls/config/v2` endpoint will include a list of all servers that are load balanced by `setSftStaticUrl` at field `sft_servers_all`. This is required to enable calls between federated instances of Wire. - -### Locale - - -#### setDefaultLocale (deprecated / ignored) - -The brig server config option `setDefaultLocale` has been replaced by `setDefaultUserLocale` and `setDefaultTemplateLocale`. Both settings are optional and `setDefaultTemplateLocale` defaults to `EN` and `setDefaultLocale` defaults to `setDefaultTemplateLocale`. If `setDefaultLocale` was not set or set to `EN` before this change, nothing needs to be done. If `setDefaultLocale` was set to any other language other than `EN` the name of the setting should be changed to `setDefaultTemplateLocale`. - -#### `setDefaultTemplateLocale` - -This option determines the default locale for email templates. The language of the email communication is determined by the user locale (see above). Only if templates of the the locale of the user do not exist or if user locale is not set the `setDefaultTemplateLocale` is used as a fallback. If not set the default is `EN`. This setting should not be changed unless a complete set of templates is available for the given language. - -``` -# [brig.yaml] -optSettings: - setDefaultTemplateLocale: en -``` - -#### `setDefaultUserLocale` - -This option determines which language to use for email communication. It is the default value if none is given in the user profile, or if no user profile exists (eg., if user is being provisioned via SCIM or manual team invitation via the team management app). If not set, `setDefaultTemplateLocale` is used instead. - -``` -# [brig.yaml] -optSettings: - setDefaultUserLocale: en -``` - -### MLS settings - -#### `setKeyPackageMaximumLifetime` - -This option specifies the maximum accepted lifetime of a key package from the moment it is uploaded, in seconds. For example, when brig is configured as follows: - -``` -# [brig.yaml] -optSettings: - setKeyPackageMaximumLifetime: 1296000 # 15 days -``` - -any key package whose expiry date is set further than 15 days after upload time will be rejected. - - -### Federated domain specific configuration settings -#### Restrict user search - -The lookup and search of users on a wire instance can be configured. This can be done per federated domain. - -```yaml -# [brig.yaml] -optSettings: - setFederationDomainConfigs: - - domain: example.com - search_policy: no_search -``` - -Valid values for `search_policy` are: -- `no_search`: No users are returned by federated searches. -- `exact_handle_search`: Only users where the handle exactly matches are returned. -- `full_search`: Additionally to `exact_handle_search`, users are found by a freetext search on handle and display name. - -If there is no configuration for a domain, it's defaulted to `no_search`. +file has moved [here](../legacy/reference/config-options.md) diff --git a/docs/reference/conversation.md b/docs/reference/conversation.md index 1d4af8569e6..23342da490f 100644 --- a/docs/reference/conversation.md +++ b/docs/reference/conversation.md @@ -1,142 +1 @@ -# Creating and populating conversations {#RefCreateAndPopulateConvs} - -_Author: Matthias Fischmann_ - ---- - -This will walk you through creating and populating a conversation -using [curl](https://curl.haxx.se/) commands and the credentials of an -ordinary user (member role). - -If you have a system for identity management like a SAML IdP, you may -be able to use that as a source of user and group information, and -write a script or a program based on this document to keep your wire -conversations in sync with your groups. - -Sidenote: in the future we may implement groups in our [SCIM -API](http://www.simplecloud.info/) to handle conversations. For the -time being, we hope you consider the approach explained here a decent -work-around. - - -## Prerequisites - -We will talk to the backend using the API that the clients use, so we -need a pseudo-user that we can authenticate as. We assume this user -has been created by other means. For the sake of testing, you could -just use the team admin (but you don't need admin privileges for this -user). - -So here is some shell environment we will need: - -```bash -export WIRE_BACKEND=https://prod-nginz-https.wire.com -export WIRE_USER=... -export WIRE_PASSWD=... -export WIRE_TEAMID=... -``` - -Now you can login and get a wire token to authenticate all further -requests: - -```bash -export BEARER=$(curl -X POST \ - --header 'Content-Type: application/json' \ - --header 'Accept: application/json' \ - -d '{"email":"'"$WIRE_USER"'","password":"'"$WIRE_PASSWD"'"}' \ - $WIRE_BACKEND/login'?persist=false' | jq -r .access_token) -``` - -This token will be good for 15 minutes; after that, just repeat. - -If you don't want to install [jq](https://stedolan.github.io/jq/), you -can just call the `curl` command and copy the access token into the -shell variable manually. - -Here is a quick test that you're logged in: - -```bash -curl -X GET --header "Authorization: Bearer $BEARER" \ - $WIRE_BACKEND/self -``` - - -## Contact requests - -If `$WIRE_USER` is in a team, all other team members are implicitly -connected to it. So for the users in your team, you don't have to do -anything here. - -TODO: contact requests to users not on the team. - - -## Conversations - -To create a converation with no users (except the pseudo-user creating it): - -```bash -export WIRE_CONV_NAME="The B-Team" -export WIRE_CONV='{ - "users": [], - "name": "'${WIRE_CONV_NAME}'", - "team": { - "managed": false, - "teamid": "'${WIRE_TEAMID}'" - }, - "receipt_mode": 0, - "message_timer": 0 -}' - -export CONV_ID=$(curl -X POST --header "Authorization: Bearer $BEARER" \ - -H "Content-Type: application/json" \ - -d "$WIRE_CONV" \ - $WIRE_BACKEND/conversations | jq -r .id) -``` - -The `users` field can contain UUIDs that need to point to existing -wire users (in your team or not), and `$WIRE_USER` needs to have an -accepted connection with them. You can extract these ids from the -corresponding SCIM user records. If in doubt, leave empty. - -You can also add and remove users once the converation has been -created: - -```bash -curl -X POST --header "Authorization: Bearer $BEARER" \ - -H "Content-Type: application/json" \ - -d '{ "users": ["b4b6a96c-70c8-11e9-99e6-f3ea044b132c", "b7293854-70c8-11e9-b620-97ff1eba6324"] }' \ - $WIRE_BACKEND/conversations/$CONV_ID/members - -curl -X DELETE --header "Authorization: Bearer $BEARER" \ - $WIRE_BACKEND/conversations/$CONV_ID/members/b9f1c786-70c8-11e9-91a6-fbeb48cdcdd1 -``` - -You can also look at one or all conversations: - -```bash -curl -X GET --header "Authorization: Bearer $BEARER" \ - $WIRE_BACKEND/conversations/ids - -curl -X GET --header "Authorization: Bearer $BEARER" \ - $WIRE_BACKEND/conversations/ - -curl -X GET --header "Authorization: Bearer $BEARER" \ - $WIRE_BACKEND/conversations/$CONV_ID/ -``` - -Finally, conversations can be renamed or deleted: - -```bash -curl -X PUT --header "Authorization: Bearer $BEARER" \ - -H "Content-Type: application/json" \ - -d '{ "name": "The C-Team" }' \ - $WIRE_BACKEND/conversations/$CONV_ID - -curl -X DELETE --header "Authorization: Bearer $BEARER" \ - $WIRE_BACKEND/teams/$WIRE_TEAMID/conversations/$CONV_ID -``` - - -## Advanced topics - -TODO: pseudo-user leaving the conv, and being added by admin for changes later. +file has moved [here](../legacy/reference/conversation.md) diff --git a/docs/reference/elastic-search.md b/docs/reference/elastic-search.md index 246988ec014..fbdf61469b9 100644 --- a/docs/reference/elastic-search.md +++ b/docs/reference/elastic-search.md @@ -1,241 +1 @@ -# Maintaining ElasticSearch - -## Update mapping - -```bash -ES_HOST= -ES_PORT= # default is 9200 -ES_INDEX= # default is directory -WIRE_VERSION= - -docker run "quay.io/wire/brig-index:$WIRE_VERSION" update-mapping \ - --elasticsearch-server "http://$ES_HOST:$ES_PORT" \ - --elasticsearch-index "$ES_INDEX" -``` - -Instead of running this in docker, this can also be done by building the `brig-index` binary from `services/brig` and executing it like this: - -```bash -brig-index update-mapping \ - --elasticsearch-server "http://$ES_HOST:$ES_PORT" \ - --elasticsearch-index "$ES_INDEX" -``` - -## Migrate Data - -```bash -ES_HOST= -ES_PORT= # default is 9200 -ES_INDEX= # default is directory -BRIG_CASSANDRA_HOST= -BRIG_CASSANDRA_PORT= -BRIG_CASSANDRA_KEYSPACE= -WIRE_VERSION= -GALLEY_HOST= -GALLEY_PORT= - -docker run "quay.io/wire/brig-index:$WIRE_VERSION" migrate-data \ - --elasticsearch-server "http://$ES_HOST:$ES_PORT" \ - --elasticsearch-index "$ES_INDEX" \ - --cassandra-host "$BRIG_CASSANDRA_HOST" \ - --cassandra-port "$BRIG_CASSANDRA_PORT" \ - --cassandra-keyspace "$BRIG_CASSANDRA_KEYSPACE" - --galley-host "$GALLEY_HOST" - --galley-port "$GALLEY_PORT" -``` - -(Or, as above, you can also do the same thing without docker.) - -## Refill ES documents from Cassandra - -This is needed if the information we keep in elastic search increases. -Also update the indices. - -```bash -ES_HOST= -ES_PORT= # default is 9200 -ES_INDEX= # default is directory -BRIG_CASSANDRA_HOST= -BRIG_CASSANDRA_PORT= -BRIG_CASSANDRA_KEYSPACE= -WIRE_VERSION= -GALLEY_HOST= -GALLEY_PORT= - -docker run "quay.io/wire/brig-index:$WIRE_VERSION" reindex \ - --elasticsearch-server "http://$ES_HOST:$ES_PORT" \ - --elasticsearch-index "$ES_INDEX" \ - --cassandra-host "$BRIG_CASSANDRA_HOST" \ - --cassandra-port "$BRIG_CASSANDRA_PORT" \ - --cassandra-keyspace "$BRIG_CASSANDRA_KEYSPACE" - --galley-host "$GALLEY_HOST" - --galley-port "$GALLEY_PORT" -``` - -Subcommand `reindex-if-same-or-newer` can be used instead of `reindex`, if you want to recreate the documents in elasticsearch regardless of their version. - -(Or, as above, you can also do the same thing without docker.) - -## Migrate to a new index - -This is needed if we want to migrate to a new index. It could be for updating -analysis settings or to change any other settings on the index which cannot be -done without restarting the index. Analysis settings can also be updated by -recreating the index. Recreating the index is simpler to do, but requires -downtime, the process is documented [below](#recreate-an-index-requires-downtime) - -This can be done in 4 steps: - -Before starting, please set these environment variables -```bash -ES_HOST= -ES_PORT= # default is 9200 -ES_SRC_INDEX= -ES_DEST_INDEX= -WIRE_VERSION= -SHARDS= -REPLICAS= -REFRESH_INTERVAL= -``` - -1. Create the new index - ```bash - docker run "quay.io/wire/brig-index:$WIRE_VERSION" create \ - --elasticsearch-server "http://$ES_HOST:$ES_PORT" \ - --elasticsearch-index "$ES_DEST_INDEX" \ - --elasticsearch-shards "$SHARDS" \ - --elasticsearch-replicas "$REPLICAS" \ - --elasticsearch-refresh-interval "$REFRESH_INTERVAL" - ``` -1. Redeploy brig with `elasticsearch.additionalWriteIndex` set to the name of new index. Make sure no old brigs are running. -1. Reindex data to the new index - ```bash - docker run "quay.io/wire/brig-index:$WIRE_VERSION" reindex-from-another-index \ - --elasticsearch-server "http://$ES_HOST:$ES_PORT" \ - --source-index "$ES_SRC_INDEX" \ - --destination-index "$ES_DEST_INDEX" - ``` - Optionally, `--timeout ` can be added to increase/decrease from the default timeout of 10 minutes. -1. Redeploy brig without `elasticsearch.additionalWriteIndex` and with `elasticsearch.index` set to the name of new index - -Now you can delete the old index. - -**NOTE**: There is a bug hidden when using this way. Sometimes a user won't get -deleted from the index. Attempts at reproducing this issue in a simpler -environment have failed. As a workaround, there is a tool in -[tools/db/find-undead](../../tools/db/find-undead) which can be used to find the -undead users right after the migration. If they exist, please run refill the ES -documents from cassandra as described [above](#refill-es-documents-from-cassandra) - -## Migrate to a new cluster - -If the ES cluster used by brig needs to be shutdown and data must be moved to a -new cluser, these steps can be taken to ensure minimal disruption to the -service. - -Before starting, please set these environment variables: - -```bash -ES_OLD_HOST= -ES_OLD_PORT= # usually 9200 -ES_OLD_INDEX= -ES_NEW_HOST= -ES_NEW_PORT= # usually 9200 -ES_NEW_INDEX= -WIRE_VERSION= -GALLEY_HOST= -GALLEY_PORT= - -# Use curl http://$ES_OLD_HOST:$ES_OLD_PORT/$ES_OLD_INDEX/_settings -# to know previous values of SHARDS, REPLICAS and REFRESH_INTERVAL -SHARDS= -REPLICAS= -REFRESH_INTERVAL= - -BRIG_CASSANDRA_HOST= -BRIG_CASSANDRA_PORT= -BRIG_CASSANDRA_KEYSPACE= -``` - -1. Create the new index - ```bash - docker run "quay.io/wire/brig-index:$WIRE_VERSION" create \ - --elasticsearch-server "http://$ES_NEW_HOST:$ES_NEW_PORT" \ - --elasticsearch-index "$ES_NEW_INDEX" \ - --elasticsearch-shards "$SHARDS" \ - --elasticsearch-replicas "$REPLICAS" \ - --elasticsearch-refresh-interval "$REFRESH_INTERVAL" - ``` -1. Redeploy brig with `elasticsearch.additionalWriteIndexUrl` set to the URL of - the new cluster and `elasticsearch.additionalWriteIndex` set to - `$ES_NEW_INDEX`. -1. Make sure no old instances of brig are running. -1. Reindex data to the new index - ```bash - docker run "quay.io/wire/brig-index:$WIRE_VERSION" migrate-data \ - --elasticsearch-server "http://$ES_NEW_HOST:$ES_NEW_PORT" \ - --elasticsearch-index "$ES_NEW_INDEX" \ - --cassandra-host "$BRIG_CASSANDRA_HOST" \ - --cassandra-port "$BRIG_CASSANDRA_PORT" \ - --cassandra-keyspace "$BRIG_CASSANDRA_KEYSPACE" - --galley-host "$GALLEY_HOST" - --galley-port "$GALLEY_PORT" - ``` -1. Remove `elasticsearch.additionalWriteIndex` and - `elasticsearch.additionalWriteIndexUrl` from brig config. Set - `elasticsearch.url` to the URL of the new cluster and `elasticsearch.index` - to the name of new index. Deploy brig with these settings. - -## Recreate an index (Requires downtime) - -When analysis settings of an index need to be changed, e.g. for changes -introduced in [#1052](https://github.com/wireapp/wire-server/pull/1052), -it is not possible to keep the index running while the changes are applied. - -To tackle this, a wire-server operator must either migrate to a new index as -documented [above](#migrate-to-a-new-index) or allow for some downtime. One -might want to choose downtime for simplicity. These steps are especially simple -to do when using [wire-server-deploy](https://github.com/wireapp/wire-server-deploy/). - -Here are the steps: - -Before starting, please set these environment variables -```bash -ES_HOST= -ES_PORT= # default is 9200 -ES_INDEX= -``` - -### Step 1: Delete the old index -```bash -curl -XDELETE http://$ES_HOST:$ES_PORT/$ES_INDEX -curl -XDELETE http://$ES_HOST:$ES_PORT/wire_brig_migrations -``` - -### Step 2: Recreate the index - -#### When using helm charts from [wire-server-deploy](https://github.com/wireapp/wire-server-deploy) - -Just redeploy the helm chart, new index will be created and after the deployment -data migrations will refill the index with users. - - -#### When not using helm charts from [wire-server-deploy](https://github.com/wireapp/wire-server-deploy) - -Set these extra environment variables: -```bash -WIRE_VERSION= -SHARDS= -REPLICAS= -REFRESH_INTERVAL= -``` -1. Create the index - ```bash - docker run "quay.io/wire/brig-index:$WIRE_VERSION" create \ - --elasticsearch-server "http://$ES_HOST:$ES_PORT" \ - --elasticsearch-index "$ES_INDEX" \ - --elastcsearch-shards "$SHARDS" \ - --elastcsearch-replicas "$REPLICAS" \ - --elastcsearch-refresh-interval "$REFRESH_INTERVAL" - ``` -1. Refill the index as documented [above](#refill-es-documents-from-cassandra) +file has moved [here](../legacy/reference/elastic-search.md) diff --git a/docs/reference/elasticsearch-migration-2021-02-16.md b/docs/reference/elasticsearch-migration-2021-02-16.md index e96c464acb9..8e17b0b9fdf 100644 --- a/docs/reference/elasticsearch-migration-2021-02-16.md +++ b/docs/reference/elasticsearch-migration-2021-02-16.md @@ -1,24 +1 @@ -# ElasticSearch migration instructions for release 2021-02-16 - -Release `2021-02-16` of `wire-server` requires an update of the ElasticSearch index of `brig`. -During the update the team member search in TeamSettings will be defunct. - -The update is triggered automatically on upgrade by the `elasticsearch-index-create` and `brig-index-migrate-data` jobs. If these jobs finish sucessfully the update is complete. - -## Troubleshooting - -In case the `elasticsearch-index-create` job fails this document describes how to create a new index. - -The index that brig is using is defined at `brig.config.elasticsearch.index` of the `wire-server` chart. We will refer to its current setting as ``. - -1. Choose a new index name that is different from ``. - We will refer to this name as ``. -2. Upgrade the release with these config changes: - - Set `brig.config.elasticsearch.additionalWriteIndex` to `` - - Set `elasticsearch-index.elasticsearch.additionalWriteIndex` to `` - and wait for completion. -3. Upgrade the release again with these config changes: - - Unset `brig.config.elasticsearch.additionalWriteIndex` - - Unset `elasticsearch-index.elasticsearch.additionalWriteIndex` - - Set `brig.config.elasticsearch.index` to `` - - Set `elasticsearch-index.elasticsearch.index` to `` +file has moved [here](../legacy/reference/elasticsearch-migration-2021-02-16.md) diff --git a/docs/reference/make-docker-and-qemu.md b/docs/reference/make-docker-and-qemu.md index 3088eda1a0d..3c15d68bec0 100644 --- a/docs/reference/make-docker-and-qemu.md +++ b/docs/reference/make-docker-and-qemu.md @@ -1,1072 +1 @@ -# About this document: -This document is written with the goal of explaining https://github.com/wireapp/wire-server/pull/622 well enough that someone can honestly review it. :) - -In this document, we're going to rapidly bounce back and forth between GNU make, bash, GNU sed, Docker, and QEMU. - -# What does this Makefile do? Why was it created? - -To answer that, we're going to have to go back to Wire-Server, specifically, our integration tests. Integration tests are run locally on all of our machines, in order to ensure that changes we make to the Wire backend do not break currently existing functionality. In order to simulate the components that wire's backend depends on (s3, cassandra, redis, etc..), we use a series of docker images. These docker images are downloaded from dockerhub, are maintained (or not maintained) by outside parties, and are built by those parties. - -When a docker image is built, even if the docker image is something like a java app, or a pile of perl/node/etc, the interpreters (openjdk, node, perl) are embedded into the image. Those interpreters are compiled for a specific processor architecture, and only run on that architecture (and supersets of it). For instance, an AMD64 image will run on only an AMD64 system, but a 386 image will run on AMD64 since AMD64 is a superset of 386. Neither of those images will run on an ARM, like a Raspberry pi. - -This Makefile contains rules that allow our Mac users to build all of the docker images locally on their machine, with some minor improvements, which will save us about 2.5G of ram during integration tests. Additionally, it contains rules for uploading these images to dockerhub for others to use, and support for linux users to build images for arm32v5, arm32v7, 386, and AMD64, despite not being on these architectures. - -It builds non-AMD64 images on linux by using QEMU, a system emulator, to allow docker to run images that are not built for the architecture the system is currently running on. This is full system emulation, like many video game engines you're probably familiar with. You know how you have to throw gobs of hardware at a machine, to play a game written for a gaming system 20 years ago? This is similarly slow. To work around this, the Makefile is written in a manner that allows us to build many docker images at once, to take advantage of the fact that most of us have many processor cores lying around doing not-all-much. - -# What does this get us? - -To start with, the resulting docker images allow us to tune the JVM settings on cassandra and elasticsearch, resulting in lower memory consumption, and faster integration tests that don't impact our systems as much. Additionally, it allows us more control of the docker images we're depending on, so that another leftpad incident on docker doesn't impact us. As things stand, any of the developers of these docker images can upload a new docker image that does Very Bad Things(tm), and we'll gladly download and run it many times a day. Building these images ourselves from known good GIT revisions prevents this. Additionally, the multi-architecture approach allows us to be one step closer to running the backend on more esoteric systems, like a Raspberry pi, or an AWS A instance, both of which are built on the ARM architecture. Or, if rumour is to be believed, the next release of MacBook Pros. :) - -# Breaking it down: - -## Docker: - -to start with, we're going to have to get a bit into some docker architecture. We all have used docker, and pretty much understand the following workflow: - -I build a docker image from a Dockerfile and maybe some additions, I upload it to dockerhub, and other people can download and use the image. I can use the locally built image directly, without downloading it from dockerhub, and I can share the Dockerfile and additions via git, on github, and allow others to build the image. - -While this workflow works well for working with a single architecture, we're going to have to introduce some new concepts in order to support the multiple architecture way of building docker files. - -### Manifest files. - -Manifest files are agenerated by docker and contain references to multiple docker images, one for each architecture a given docker image has been built for. Each image in the manifest file is tagged with the architecture that the image is built for. - -Docker contains just enough built-in logic to interpret a manifest file on dockerhub, and download an image that matches the architecture that docker was built for. When using a manifest file, this is how docker determines what image to download. - -### A Manifest centric Workflow: - -If you're building a docker image for multiple architectures, you want a Manifest, so that docker automatically grabs the right image for the user's machine. This changes our workflow from earlier quite a bit: - -I build a docker image from a Dockerfile, and I build other images from slightly different versions of this Dockerfile (more on this later). I tag these images with a suffix, so that I can tell them apart. I upload the images to dockerhub, retaining the tags that differentiate the diffenent versions from each other. I create a manifest file, referring to the images that have been pushed to DockerHub, and upload the manifest file to DockerHub. People can download and use the image from dockerhub by refering to the tag of the manifest file. I can share the Dockerfile and additions via git, on dockerhub, and others can build their own images from it. - -#### What does this look like? - -All of us on the team are using AMD64 based machines, so in this example, we're going to build one image for AMD64, and one for it's predecessor architecture, I386. We're going to build the SMTP server image we depend on, from https://hub.docker.com/r/namshi/smtp. We're going to use a known safe git revision, and use some minor GNU sed to generate architecture dependent Dockerfiles from the Dockerfile in git. Everyone should be able to do this on your laptops. - -```bash -$ git clone https://github.com/namshi/docker-smtp.git smtp -Cloning into 'smtp'... -remote: Enumerating objects: 4, done. -remote: Counting objects: 100% (4/4), done. -remote: Compressing objects: 100% (4/4), done. -remote: Total 126 (delta 0), reused 0 (delta 0), pack-reused 122 -Receiving objects: 100% (126/126), 26.57 KiB | 269.00 KiB/s, done. -Resolving deltas: 100% (61/61), done. -$ cd smtp -$ git reset --hard 8ad8b849855be2cb6a11d97d332d27ba3e47483f -HEAD is now at 8ad8b84 Merge pull request #48 from zzzsochi/master -$ cat Dockerfile | sed "s/^\(MAINTAINER\).*/\1 Julia Longtin \"julia.longtin@wire.com\"/" | sed "s=^\(FROM \)\(.*\)$=\1i386/\2=" > Dockerfile-386 -$ cat Dockerfile | sed "s/^\(MAINTAINER\).*/\1 Julia Longtin \"julia.longtin@wire.com\"/" | sed "s=^\(FROM \)\(.*\)$=\1\2=" > Dockerfile-amd64 -$ docker build -t julialongtin/smtp:0.0.9-amd64 -f Dockerfile-amd64 - -$ docker build -t julialongtin/smtp:0.0.9-386 -f Dockerfile-386 - -$ docker push julialongtin/smtp:0.0.9-amd64 - -$ docker push julialongtin/smtp:0.0.9-386 -] 271.46K --.-KB/s in 0.07s - -2019-03-06 14:27:39 (3.65 MB/s) - ‘sash_3.8-5_armel.deb’ saved [277976/277976] -$ -``` - -This deb will not install on our machine, so we're going to manually take it apart, to get the sash binary out of it. - -```bash -$ mkdir tmp -$ cd tmp -$ ar x ../sash_3.8-5_armel.deb -$ ls - control.tar.xz data.tar.xz -$ tar -xf data.tar.gz -$ ls -la bin/sash --rwxr-xr-x 1 demo demo 685348 Jun 9 2018 bin/sash -``` - -to verify what architecture this binary is built for, use the 'file' command. -```bash -$ file bin/sash -bin/sash: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=20641a8ca21b2c320ea7e6079ec88b857c7cbcfb, stripped -$ -``` - -now we can run this, and even run Arm64 programs that are on our own machine using it. -```bash -$ bin/sash -Stand-alone shell (version 3.8) -> file bin/sash -bin/sash: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=20641a8ca21b2c320ea7e6079ec88b857c7cbcfb, stripped -> ls -bin usr -> uname -a -Linux boxtop 4.9.0-8-amd64 #1 SMP Debian 4.9.144-3 (2019-02-02) x86_64 GNU/Linux -> whoami -demo -> -``` - -## QEMU, BinFmt, and Docker (Oh my!) - -After following the directions in the last two sections, you've created two docker images (one for i386, one for AMD64), created a manifest referring to them, set up for linux to load qemu and use it, and launched a binary for another architecture. - -Creating non-native docker images can now be done very similar to how i386 was done earlier. - -Because you are using a system emulator, your docker builds for non-x86 will be slower. additionally, the emulators are not perfect, so some images won't build. finally, code is just less tested on machines that are not an AMD64 machine, so there are generally more bugs. - -### Arm Complications: -The 32 bit version of arm is actually divided into versions, and not all linux distributions are available for all versions. arm32v5 and arm32v7 are supported by debian, while arm32v6 is supported by alpine. This variant must be specified during manifest construction, so to continue with our current example, these are the commands for tagging the docker images for our arm32v5 and arm32v7 builds of smtp: -```bash -$ docker manifest annotate julialongtin/smtp:0.0.9 julialongtin/smtp:0.0.9-arm32v5 --arch arm --variant 5 -$ docker manifest annotate julialongtin/smtp:0.0.9 julialongtin/smtp:0.0.9-arm32v7 --arch arm --variant 7 -``` - - -# Into the GNU Make Abyss - -Now that we've done all of the above, we should be capable of working with docker images independent of the architecture we're targeting. Now, into the rabit hole we go, automating everything with GNU Make - -## Why Make? -GNU make is designed to build targets by looking at the environment it's in, and executing a number of rules depending on what it sees, and what it has been requested to do. The Makefile we're going to look through does all of the above, along with making some minor changes to the docker images. It does this in parallel, calling as many of the commands at once as possible, in order to take advantage of idle cores. - -## Using the Makefile - -Before we take the Makefile apart, let's go over using it. - -This Makefile is meant to be used in four ways: building a set of images, pushing (and building) a set of images, building a single image. It follows the manifest workflow we documented earlier. - -By default, running 'make' in the same directory as the Makefile (assuming you've set all of the above up correctly) will attempt to build and push all of the docker images the makefile knows about to dockerhub. If you want this to work, you need to create a dockerhub account, use 'docker login' to log your local instance of docker in to dockerhub, then you need to create a repository for each docker image. - -To get a list of the names of the docker images this Makefile knows about, run 'make names'. -```bash -$ make names -Debian based images: -airdock_fakesqs airdock_rvm airdock_base smtp dynamodb_local cassandra -Alpine based images: -elasticsearch java_maven_node_python localstack minio -$ -``` - -The list of names is divided into two groups. one group is for images based on debian, and the other is for images based on alpine. This makefile can only build for one of these two distributions at once. - -Since no-one wants to click through dockerhub to create repositories, let's just build docker images locally, for now. - -Make looks at it's environment in order to decide what to do, so here are some environment variables that we're going to use. all of these variables have default values, so we're only going to provide a few of them. - -- `ARCHES`: the list of architectures we're going to attempt docker builds for. Mac users should supply "386 AMD64" to this, as they have no binfmt support. -- `DIST`: the distribution we're going to build for. this can be either DEBIAN or ALPINE. -- `DOCKER`_USERNAME: our username on dockerhub. -- `DOCKER`_EMAIL: Our email address, as far as dockerhub is concerned. -- `DOCKER`_REALNAME: again, our name string that will be displayed in DockerHub. -- `SED`: which sed binary to use. Mac users should install GSED, and pass the path to it in this variable. - -To build all of the debian based images locally on my machine, I run -```bash -make DIST=DEBIAN DOCKER_USERNAME=julialongtin DOCKER_EMAIL=julia.longtin@wire.com DOCKER_REALNAME='Julia Longtin' build-all -j". -``` - -What's the -j for? adding a '-j' to the command line causes make to execute in parallel. That's to say, it will try to build ALL of the images at once, taking care to build images that are dependencies of other images before building the images that depend on them. - -Note that since we are building the images without pushing them to DockerHub, no manifest files are generated. - -If we want to use these images in our docker compose, we can edit the docker compose file, and refer to the image we want with it's architecture suffix attached. This will make docker-compose use the local copy, instead of hitting DockerHub, grabbing the manifest, and using an image from there. for instance, to use the local cassandra image I just built, I would edit the docker-compose.yaml file in our wire-server repo, and make the cassandra section look like the following: - -``` - cassandra: - container_name: demo_wire_cassandra - #image: cassandra:3.11.2 - image: julialongtin/cassandra:0.0.9-amd64 - ports: - - "127.0.0.1:9042:9042" - environment: -# what's present in the jvm.options file by default. -# - "CS_JAVA_OPTIONS=-Xmx1024M -Xms1024M -Xmn200M" - - "CS_JVM_OPTIONS=-Xmx128M -Xms128M -Xmn50M" - networks: - - demo_wire -``` - -To remove all of the git repositories containing the Dockerfiles we download to build these images, we can run `make clean`. There is also the option to run `make cleandocker` to REMOVE ALL OF THE DOCKER IMAGES ON YOUR MACHINE. careful with that one. Note that docker makes good use of caching, so running 'make clean' and the same make command you used to build the images will complete really fast, as docker does not actually need to rebuild the images. - -## Reading through the Makefile - -OK, now that we have a handle on what it does, and how to use it, let's get into the Makefile itsself. - -A Makefile is a series of rules for performing tasks, variables used when creating those tasks, and some minimal functions and conditional structures. Rules are implemented as groups of bash commands, where each line is handled by a new bash interpreter. Personally, I think it 'feels functiony', only without a type system and with lots of side effects. Like if bash tried to be functional. - -### Variables - -#### Overrideable Variables -the make language has multiple types of variables and variable assignments. To begin with, let's look at the variables we used in the last step. -```bash -$ cat Makefile | grep "?=" -DOCKER_USERNAME ?= wireserver -DOCKER_REALNAME ?= Wire -DOCKER_EMAIL ?= backend@wire.com -TAGNAME ?= :0.0.9 -DIST ?= DEBIAN -LOCALARCH ?= $(call dockerarch,$(LOCALDEBARCH)) - ARCHES ?= $(DEBARCHES) - NAMES ?= $(DEBNAMES) - ARCHES ?= $(ALPINEARCHES) - NAMES ?= $(ALPINENAMES) -SED ?= sed -SMTP_COMMIT ?= 8ad8b849855be2cb6a11d97d332d27ba3e47483f -DYNAMODB_COMMIT ?= c1eabc28e6d08c91672ff3f1973791bca2e08918 -ELASTICSEARCH_COMMIT ?= 06779bd8db7ab81d6706c8ede9981d815e143ea3 -AIRDOCKBASE_COMMIT ?= 692625c9da3639129361dc6ec4eacf73f444e98d -AIRDOCKRVM_COMMIT ?= cdc506d68b92fa4ffcc7c32a1fc7560c838b1da9 -AIRDOCKFAKESQS_COMMIT ?= 9547ca5e5b6d7c1b79af53e541f8940df09a495d -JAVAMAVENNODEPYTHON_COMMIT ?= 645af21162fffd736c93ab0047ae736dc6881959 -LOCALSTACK_COMMIT ?= 645af21162fffd736c93ab0047ae736dc6881959 -MINIO_COMMIT ?= 118270d76fc90f1e54cd9510cee9688bd717250b -CASSANDRA_COMMIT ?= 064fb4e2682bf9c1909e4cb27225fa74862c9086 -``` - -The '?=' assignment operator is used to provide a default value. When earlier, we ran make as "make DIST=DEBIAN DOCKER_USERNAME=julialongtin DOCKER_EMAIL=julia.longtin@wire.com DOCKER_REALNAME='Julia Longtin' build-all -j", we were overriding those values. the Make interpreter will use values provided on the command line, or values we have used 'export' to place into our shell environment. - -LOCALARCH and the assignments for ARCHES and NAMES are a bit different. LOCALARCH is a function call, and the ARCHES and NAMES are emdedded in conditional statements. We'll cover those later. - -Note the block of COMMIT IDs. This is in case we want to experiment with newer releases of each of the docker images we're using. Fixing what we're using to a commit ID makes it much harder for an upstream source to send us malicious code. - -#### Non-Overrideable Variables -The following group of variables use a different assignment operator, that tells make not to look in the environment first. -```bash -$ cat Makefile | grep ":=" -USERNAME := $(DOCKER_USERNAME) -REALNAME := $(DOCKER_REALNAME) -EMAIL := $(DOCKER_EMAIL) -STRETCHARCHES := arm32v5 arm32v7 386 amd64 arm64v8 ppc64le s390x -JESSIEARCHES := arm32v5 arm32v7 386 amd64 -DEBARCHES := arm32v5 arm32v7 386 amd64 -JESSIENAMES := airdock_fakesqs airdock_rvm airdock_base smtp -STRETCHNAMES := dynamodb_local cassandra -DEBNAMES := $(JESSIENAMES) $(STRETCHNAMES) -ALPINEARCHES := amd64 386 arm32v6 -ALPINENAMES := elasticsearch java_maven_node_python localstack minio -PREBUILDS := airdock_rvm-airdock_base airdock_fakesqs-airdock_rvm localstack-java_maven_node_python -NOMANIFEST := airdock_rvm airdock_fakesqs localstack -LOCALDEBARCH := $(shell [ ! -z `which dpkg` ] && dpkg --print-architecture) -BADARCHSIM := localstack-arm32v6 java_maven_node_python-arm32v6 dynamodb_local-386 -$ -``` - -The first three variable assignments are referring to other variables. These basically exist as alias, to make our make rules denser later. - -STRETCHARCHES and JESSIEARCHES contain the list of architectures that dockerhub's debian stretch and jessie images provide. DEBARCHES defines what architectures we're going to build, for our debian targets. STRETCHARCHES and DEBIANARCHES only exist to make it visible to readers of the Makefile which images CAN be built for which architectures. - -JESSIENAMES and STRETCHNAMES are used similarly, only they are actually referred to by DEBNAMES, to provide the list of debian based images that can be built. - -ALPINEARCHES and ALPINENAMES work similarly, and are used when we've provided "DIST=ALPINE". We do not divide into seperate variables quite the same way as debian, because all of our alpine images are based on alpine 3.7. - -PREBUILDS contains our dependency map. essentially, this is a set of pairs of image names, where the first image mentioned depends on the second image. so, airdock_rvm depends on airdock_base, where airdock_fakesqs depends on airdock_rvm, etc. this means that our docker image names may not contain `-`s. Dockerhub allows it, but this makefile needed a seperator... and that's the one I picked. - -BADARCH is similar, pairing the name of an image with the architecture it fails to build on. This is so I can blacklist things that don't work yet. - -LOCALDEBARCH is a variable set by executing a small snippet of bash. The snippet makes sure dpkg is installed (the debian package manager), and uses dpkg to determine what the architecture of your local machine is. As you remember from when we were building docker images by hand, docker will automatically fetch an image that is compiled for your current architecture, so we use LOCALDEBARCH later to decide what architectures we need to fetch with a prefix or postfix, and which we can fetch normally. - -NOMANIFEST lists images that need a work-around for fetching image dependencies for specific architectures. You know how we added the name of the architecture BEFORE the image name in the dockerfiles? well, in the case of the dependencies of the images listed here, dockerhub isn't supporting that. DockerHub is supporting that form only for 'official' docker images, like alpine, debian, etc. as a result, in order to fetch an architecture specific version of the dependencies of these images, we need to add a - suffix. like -386 -arm32v7, etc. - -### Conditionals -We don't make much use of conditionals, but there are three total uses in this Makefile. let's take a look at them. - -In order to look at our conditionals (and many other sections of this Makefile later), we're going to abuse sed. If you're not comfortable with the sed shown here, or are having problems getting it to work, you can instead just open the Makefile in your favorite text editor, and search around. I abuse sed here for both brevity, and to encourage the reader to understand complicated sed commands, for when we are using them later IN the Makefile. - -SED ABUSE: -to get our list of conditionals out of the Makefile, we're going to use some multiline sed. specifically, we're going to look for a line starting with 'ifeq', lines starting with two spaces, then the line following. - -```bash -$ cat Makefile | sed -n '/ifeq/{:n;N;s/\n /\n /;tn;p}' -ifeq ($(LOCALARCH),) - $(error LOCALARCH is empty, you may need to supply it.) - endif -ifeq ($(DIST),DEBIAN) - ARCHES ?= $(DEBARCHES) - NAMES ?= $(DEBNAMES) -endif -ifeq ($(DIST),ALPINE) - ARCHES ?= $(ALPINEARCHES) - NAMES ?= $(ALPINENAMES) -endif -$ -``` - -There's a lot to unpack there, so let's start with the simple part, the conditionals. -The conditionals are checking for equality, in all cases. -First, we check to see if LOCALARCH is empty. This can happen if dpkg was unavailable, and the user did not supply a value on the make command line or in the user's bash environment. if that happens, we use make's built in error function to display an error, and break out of the Makefile. -The second and third conditionals decide on the values of ARCHES and NAMES. Earlier, we determined the default selection for DIST was DEBIAN, so this pair just allows the user to select ALPINE instead. note that the variable assignments in the conditionals are using the overrideable form, so the end user can override these on make's command line or in the user's environment. mac users will want to do this, since they don't have QEMU available in the same form, and are limited to building X86 and AMD64 architecture. - -Note that conditionals are evaluated when the file is read, once. This means that we don't have the ability to use them in our rules, or in our functions, and have to abuse other operations in 'functionalish' manners... - -Now, back to our sed abuse. -SED is a stream editor, and quite a powerful one. In this case, we're using it for a multi-line search. we're supplying the -n option, which squashes all output, except what sed is told specificly to print something with a command. -Let's look at each of the commands in that statement seperately. -```sed -# find a line that has 'ifeq' in it. -/ifeq/ -# begin a block of commands. every command in the block should be seperated by a semicolon. -{ -# create an anchor, that is to say, a point that can be branched to. -:n; -# Append the next line into the parameter space. so now, for the first block, the hold parameter space would include "ifeq ($(LOCALARCH),)\n $(error LOCALARCH is empty, you may need to supply it.)". -N; -# Replace the two spaces in the parameter space with one space. -s/\n /\n /; -# If the previous 's' command found something, and changed something, go to our label. -tn; -# print the contents of the parameter space. -p -# close the block of commands. -} -``` -... Simple, right? - -note that the contents above can be stored to a file, and run with sed's "-f" command, for more complicated sed scripts. Sed is turing complete, so... things like tetris have been written in it. My longest sed scripts do things like sanity check OS install procedures, or change binaryish protocols into xmlish forms. - -### Functions -Make has a concept of functions, and the first two functions we use are a bit haskell inspired. - -SED ABUSE: -To get a list of the functions in our makefile, we're going to use a bit more traditional sed. specifically, we're going to look for lines that start with a number of lowercase characters that are immediately followed by an '=' sign. - -```bash -$ cat Makefile | sed -n '/^[a-z]*=/p' -dockerarch=$(patsubst i%,%,$(patsubst armel,arm32v5,$(patsubst armhf,arm32v7,$(patsubst arm64,arm64v8,$(1))))) -fst=$(word 1, $(subst -, ,$(1))) -snd=$(word 2, $(subst -, ,$(1))) -goodarches=$(filter-out $(call snd,$(foreach arch,$(ARCHES),$(filter $(1)-$(arch),$(BADARCHSIM)))),$(ARCHES)) -nodeps=$(filter-out $(foreach target,$(NAMES),$(call snd,$(foreach dependency,$(NAMES),$(filter $(target)-$(dependency),$(PREBUILDS))))),$(NAMES)) -maniarch=$(patsubst %32,%,$(call fst,$(subst v, ,$(1)))) -manivariant=$(foreach variant,$(word 2, $(subst v, ,$(1))), --variant $(variant)) -archpostfix=$(foreach arch,$(filter-out $(filter-out $(word 3, $(subst -, ,$(filter $(call snd,$(1))-%-$(call fst,$(1)),$(foreach prebuild,$(PREBUILDS),$(prebuild)-$(call fst,$(1)))))),$(LOCALARCH)),$(call fst,$(1))),-$(arch)) -archpath=$(foreach arch,$(patsubst 386,i386,$(filter-out $(LOCALARCH),$(1))),$(arch)/) -$ -``` - -These are going to be a bit hard to explain in order, especially since we haven't covered where they are being called from. Let's take them from simplest to hardest, which happens to co-incide with shortest, to longest. - -The fst and snd functions are what happens when a haskell programmer is writing make. You remember all of the pairs of values earlier, that were seperated by a single '-' character? these functions return either the first, or the second item in the pair. Let's unpack 'fst'. -fst uses the 'word' function of make to retrieve the first word from "$(subst -, ,$(1))". the 'subst' function substitutes a single dash for a single space. this seperates a - pair into a space seperated string. $(1) is the first argument passed to this function. -snd works similarly, retrieving from our pair. - -The next easiest to explain function is 'maniarch'. It returns the architecture string that we use when annotating a docker image. When we refer to an architecture, we use a string like 'amd64' or 'arm32v6', but docker manifest wants just 'arm' 'amd64' or '386'. -maniarch first uses the 'patsubst' command to replace "anystring32" with "anystring". this removes the 32 from arm32. It's given the result of $(call fst,$(subst v, ,$(1)))) as a string to work with. -$(call fst,$(subst v, ,$(1)))) calls our 'fst' function, giving it the result of us substituting 'v' for ' ' in the passed in argument. in the case of arm32v6, it seperates the string into "arm32 6". Note that instead of calling fst, we could have just used 'word 1' like we did in fst. This is a mistake on my part, but it works regardless, because of the way fst is built. as before, $(1) is the argument passed into our function. - -manivariant has a similar function to maniarch. It's job is to take an architecture name (amd64, arm32v5, etc...), and if it has a 'v', to return the '--variant ' command line option for our 'docker manifest anotate'. -manivariant starts by using make's 'foreach' function. this works by breaking it's second argument into words, storing them into the variable name given in the first argument, and then generating text using the third option. this is a bit abusive, as we're really just using it as "if there is a variant, add --variant " structure. -The first argument of foreach is the name of a variable. we used 'variant' here. the second argument in this case properly uses word, and subst to return only the content after a 'v' in our passed in argument, or emptystring. the third option is ' --variant $(variant)', using the variable defined in the first parameter of foreach to create " --variant 5" if this is passed "arm32v5", for instance. - -archpath is similar in structure to manivariant. In order to find a version of a docker image that is appropriate for our non-native architectures, we have to add the 'archname/' string to the path to the image we're deriving from, in our Dockerfile. This function returns that string. We start by using foreach in a similar method as manivariant, to only return a string if the second argument to foreach evaluates to content. In our second argument, we begin by performing a patsubst, replacing a '386' with an 'i386' if it's found in the patsubst argument. This is because on dockerhub, official images of different architectures are actually stored in a series of machine maintained accounts, and an account name can't start with a number. therefore, 386 images are stored under a user called 'i386'. As an argument to the patsubst, we're providing our first usage of filter-out. it's used here so that if the local architecture was supplied to this function, the string will return empty in section 2 of our foreach, and therefore the third section won't even be evaluated. - -our next function to explain is 'goodarches'. This function is passed an image name, and returns all of the arches from our architecture list that that image can be built for. It basically searches BADARCHSIM from earlier, and removes an architecture from the returned copy of ARCHES if a - pair for that architecture exists. We use filter-out to remove anything returned from it's second argument from the ARCHES list we provide as it's third argument. The second argument to filter-out uses snd to seperate the architecture name from a string found, and uses foreach and filter to search BADARCHSIM for all possible combinations between the passed in image name, and all of the architectures. - -dockerarch is a bit simpler than the last few. it takes the debian architecture name, replacing it with the docker architecture name, using a series of nested patsubst substititions. - -Unlike our earlier functions, nodeps does not require an argument. It's function is to return a list of images from NAMES that do not have any dependencies. To do this, we start with a filter-out of NAMES, then use a pair of nested foreach statements, both searching through NAMES, and constructing all combinations of -. This value is looked for in PREBUILDS, and if a combination is found, we use snd to return the dependency to filter-out. this is probably overly complicated, and can likely be shortened by the use of patsubst. "it works, ship it." - -Finally, we get to archpostfix. archpostfix has a similar function to archpath, only it provides a - for the end of the image path if the DEPENDENCY of this image is not an official image, and therefore can not be found by adding an 'arch/' postfix. This is long, and probably also a candidate for shortening. Reading your way through this one is an exercise for when the reader wants a reverse polish notation headache. - - -To summarize the Make functions we've (ab)used in this section: -``` -$(word 1,string of words) # return the Nth word in a space separated string. -$(subst -, ,string-of-words) # replace occurances of '-' with ' ' in string. -$(patsubst string%,%string) # replace a patern with another patern, using % as a single wildcard. -$(call function,argument) # function calling. -$(foreach var,string,$(var)) # iterate on a space seperated string, evaluating the last argument with var set to each word in string. -$(filter word,word list) # return word if it is found in word list. -$(filter-out word, word list) # return word list without word. -``` - -Now after all of that, let's go through the SED command we last used. Remember that? -```bash -$ cat Makefile | sed -n '/^[a-z]*=/p' -``` -Again, we're going to use sed in '-n' mode, supressing all output except the output we are searching for. /PATTERN/ searches the lines of the input for a pattern, and if it's found, the command afterward is executed, which is a 'p' for print, in this case. the patern given is '^[a-z]*='. The '^' at the beginning means 'look for this patern at the beginning of the line, and the '=' at the end is the equal sign we were looking for. '[a-z]*' is us using a character class. character classes are sedspeak for sets of characters. they can be individually listed, or in this case, be a character range. the '*' after the character class just means "look for these characters any number of times". technically, that means a line starting in '=' would work (since zero is any number of times), but luckily, our file doesn't contain lines starting with =, as this is not valid make syntax. - -### Rules. - -Traditionally, makefiles are pretty simple. they are used to build a piece of software on your local machine, so you don't have to memorize all of the steps, and can type 'make', and have it just done. A simple Makefile looks like the following: -```make -CC=gcc -CFLAGS=-I. -DEPS = hellomake.h - -%.o: %.c $(DEPS) - $(CC) -c -o $@ $< $(CFLAGS) - -hellomake: hellomake.o hellofunc.o - $(CC) -o hellomake hellomake.o hellofunc.o - -clean: - rm hellomake hellomake.o hellofunc.o -``` -This example Makefile has some variables, and rules, that are used to build a C program into an executable, using GCC. - -Our Makefile is much more advanced, necessatating this document, to ensure maintainability. - - -A single make rule is divided into three sections: what you want to build, what you need to build first, and the commands you run to build the thing in question: -```make -my_thing: things I need first - bash commands to build it - -target: prerequisites - recipe line 1 -``` - -The commands to build a thing (recipe lines) are prefaced with a tab character, and not spaces. Each line is executed in a seperate shell instance. - - -#### The roots of the trees - -In the section where we showed you how to use our Makefile, we were calling 'make' with an option, such as push-all, build-smtp, names, or clean. We're now going to show you the rules that implement these options. - -SED ABUSE: -This time, we're going to add the -E command to sed. this kicks sed into the 'extended regex' mode, meaning for our purposes, we don't have to put a \ before a ( or a ) in our regex. we're then going to use a patern grouping, to specify that we want either the clean or names rules. we're also going to swap the tabs for spaces, to prevent our substitution command from always matching, and not even visibly change the output. total cheating. -```bash -$ cat Makefile | sed -n -E '/^(clean|names)/{:n;N;s/\n\t/\n /;tn;p}' -clean: - rm -rf elasticsearch-all airdock_base-all airdock_rvm-all airdock_fakesqs-all cassandra-all $(DEBNAMES) $(ALPINENAMES) - -cleandocker: - docker rm $$(docker ps -a -q) || true - docker rmi $$(docker images -q) --force || true - -names: - @echo Debian based images: - @echo $(DEBNAMES) - @echo Alpine based images: - @echo $(ALPINENAMES) -``` - -Most Makefiles change their environment. Having changed their environment, most users want a quick way to set the environment back to default, so they can make changes, and build again. to enable this, as a convention, most Makefiles have a 'clean' rule. Ours remove the git repos that we build the docker images from. note the hardcoded list of '-all' directories: these are the git repos for images where the git repo does not simply have a Dockerfile at the root of the repo. In those cases, our rules that check out the repos check them out to -all, then do Things(tm) to create a /Dockerfile. - -cleandocker is a rule I use on my machine, when docker images have gotten out of control. it removes all of the docker images on my machine, and is not meant to be regularly run. - -names displays the names of the images this Makefile knows about. It uses a single @ symbol at the beginning of the rules. this tells make that it should NOT display the command that make is running, when make runs it. - -OK, that covers the simple make rules, that have no dependencies, or parameters. Now let's take a look at our build and push rules. these are the 'top' of a dependency tree, which is to say they depend on things, that depend on things... that do the think we've asked for. - -```bash -$ cat Makefile | sed -n -E '/^(build|push|all)/{:n;N;s/\n\t/\n /;tn;p}' -all: $(foreach image,$(nodeps),manifest-push-$(image)) - -build-%: $$(foreach arch,$$(call goodarches,%),create-$$(arch)-$$*) - @echo -n - -build-all: $(foreach image,$(nodeps),build-$(image)) - -push-%: manifest-push-% - @echo -n - -push-all: $(foreach image,$(nodeps),manifest-push-$(image)) -$ -``` - -Lets take these simplest to most complex. - -push-% is the rule called when we run 'make push-'. It depends on manifest-push-%, meaning that make will take whatever you've placed after the 'push-', look for a rule called manifest-push-, and make sure that rule completes, before trying to execute this rule. Executing this rule just executes nothing, and in reality, the '@echo -n" exists to allow the push-% rule to be executed. By default, make considers wildcard rules as phony, meaning they cannot be called from the command line, and must be called from a rule with no wildcarding. - -push-all is allowed to have no commands, because it's name contains no wildcard operator. In it's dependency list, we're using a foreach loop to go through our list of images that have no dependencies, and ask for manifest-push- to be built. - -all is identical to push-all. I could have just depended on push-all, and saved some characters here. - -build-all operates similar to push-all, only it asks for build- to be built for all of the no-dependency images. - -build-% combines the approach of push-% and build-all. It uses foreach to request the build of create--, which builds one docker image for each architecture that we know this image will build on. This is our first exposure to $$ structures, so let's look at those a bit. - -By default, make allows for one % in the build-target, and one % in the dependencies. it takes what it matches the % against in the build-target, and substitutes the first % found in the dependency list with that content. so, what do you do if you need to have the thing that was matched twice in the dependency list? enter .SECONDEXPANSION. - -```bash -$ cat Makefile | sed -n -E '/^(.SECOND)/{:n;N;s/\n\t/\n /;tn;p}' | less -.SECONDEXPANSION: - -``` - -.SECONDEXPANSION looks like a rule, but really, it's a flag to make, indicating that dependency lists in this Makefile should be expanded twice. During the first expansion, things will proceed as normal, and everything with two dollar signs will be ignored. during the second expansion things that were delayed by using two dollar signs are run, AND a set of variables that is normally available in the 'recipe' section. In the case we're looking at, this means that during the first expansion, only the "%" character will be interpreted. during the second expansion the foreach and call will actually be executed, and the $$* will be expanded the same way as $* will be in the recipe section, namely, exactly identical to the % expansion in the first expansion. This effectively gives us two instances of %, the one expanded in the first expansion, and $$* expanded in the second expansion. - -build-% also uses the same 'fake recipe' trick as push-%, that is, having a recipe that does nothing, to trick make into letting you run this. - -#### One Level Deeper - -The rules you've seen so far were intended for user interaction. they are all rules that the end user of this Makefile picks between, when deciding what they want this makefile to do. Let's look at the rules that these depend on. - -```bash -$ cat Makefile | sed -n -E '/^(manifest-push)/{:n;N;s/\n\t/\n /;tn;p}' -manifest-push-%: $$(foreach arch,$$(call goodarches,$$*), manifest-annotate-$$(arch)-$$*) - docker manifest push $(USERNAME)/$*$(TAGNAME) - -$ -``` - -manifest-push-% should be relatively simple for you now. the only thing new here, is you get to see $* used in the construction of our docker manifest push command line. Let's follow the manifest creation down a few more steps. - -```bash -$ cat Makefile | sed -n -E '/^(manifest-ann|manifest-crea)/{:n;N;s/\n\t/\n /;tn;p}' -manifest-annotate-%: manifest-create-$$(call snd,$$*) - docker manifest annotate $(USERNAME)/$(call snd,$*)$(TAGNAME) $(USERNAME)/$(call snd,$*)$(TAGNAME)-$(call fst,$*) --arch $(call maniarch,$(call fst,$*)) $(call manivariant,$(call fst,$*)) - -manifest-create-%: $$(foreach arch,$$(call goodarches,%), upload-$$(arch)-$$*) - docker manifest create $(USERNAME)/$*$(TAGNAME) $(patsubst %,$(USERNAME)/$*$(TAGNAME)-%,$(call goodarches,$*)) --amend - -``` - -manifest-push depends on manifest-annotate, which depends on manifest-create, that depends on upload-... so when make tries to push a manifest, it makes sure an image has been uploaded, then creates a manifest, then annotates the manifest. We're basically writing rules for each step of our manifest, only backwards. continuing this pattern, the last thing we will depend on will be the rules that actually download the dockerfiles from git. - -#### Dependency Resolving - -We've covered the entry points of this Makefile, and the chained dependencies that create, annotate, and upload a manifest file. now, we get into two seriously complicated sets of rules, the upload rules and the create rules. These accomplish their tasks of uploading and building docker containers, but at the same time, they accomplish our dependency resolution. Let's take a look. - -```bash -$ cat Makefile | sed -n -E '/^(upload|create|my-|dep)/{:n;N;s/\n\t/\n /;tn;p}' - -upload-%: create-% $$(foreach predep,$$(filter $$(call snd,%)-%,$$(PREBUILDS)), dep-upload-$$(call fst,$$*)-$$(call snd,$$(predep))) - docker push $(USERNAME)/$(call snd,$*)$(TAGNAME)-$(call fst,$*) | cat - -dep-upload-%: create-% $$(foreach predep,$$(filter $$(call snd,%)-%,$$(PREBUILDS)), dep-subupload-$$(call fst,$$*)-$$(call snd,$$(predep))) - docker push $(USERNAME)/$(call snd,$*)$(TAGNAME)-$(call fst,$*) | cat - -dep-subupload-%: create-% - docker push $(USERNAME)/$(call snd,$*)$(TAGNAME)-$(call fst,$*) | cat - -create-%: Dockerfile-$$(foreach target,$$(filter $$(call snd,$$*),$(NOMANIFEST)),NOMANIFEST-)$$* $$(foreach predep,$$(filter $$(call snd,%)-%,$(PREBUILDS)), depend-create-$$(call fst,$$*)-$$(call snd,$$(predep))) - cd $(call snd,$*) && docker build -t $(USERNAME)/$(call snd,$*)$(TAGNAME)-$(call fst,$*) -f Dockerfile-$(call fst,$*) . | cat - -depend-create-%: Dockerfile-$$(foreach target,$$(filter $$(call snd,$$*),$(NOMANIFEST)),NOMANIFEST-)$$* $$(foreach predep,$$(filter $$(call snd,%)-%,$(PREBUILDS)), depend-subcreate-$$(call fst,$$*)-$$(call snd,$$(predep))) - cd $(call snd,$*) && docker build -t $(USERNAME)/$(call snd,$*)$(TAGNAME)-$(call fst,$*) -f Dockerfile-$(call fst,$*) . | cat - -depend-subcreate-%: Dockerfile-$$(foreach target,$$(filter $$(call snd,$$*),$(NOMANIFEST)),NOMANIFEST-)$$* - cd $(call snd,$*) && docker build -t $(USERNAME)/$(call snd,$*)$(TAGNAME)-$(call fst,$*) -f Dockerfile-$(call fst,$*) . | cat - -$ -``` - -First, let's tackle the roles of these rules. the *upload* rules are responsible for running docker push, while the *create* rules are responsible for running docker build. All of the upload rules depend on the first create rule, to ensure what they want to run has been built. - -these rules are setup in groups of three: - -upload-% and create-% form the root of these groups. upload-% depends on create-%, and create-% depends on the creation of a Dockerfile for this image, which is the bottom of our dependency tree. - -upload-%/create-% depend on two rules: dep-upload-%/depend-create-%, which handle the upload/create for the image that THIS image depends on. There are also dep-subupload-% and dep-subcreate-% rules, to handle the dependency of the dependency of this image. - -This dependency-of, and dependency-of-dependency logic is necessary because Make will not let us run a recursive rule: no rule can be in one branch of the dependency graph more than once. so instead, the root of our dependency tree either starts with a single image, or with a list of images that are the root of their own dependency graphs. - - -Now let's look at the rules themselves. -upload-% has a dependency on create-%, to ensure what it wantas to upload already exists. additionally, it has a dependency that uses foreach and filter to look through the list of PREBUILDS, and depend on dep-upload-- for any images this image depends on. - -dep-upload-% is virtually identical to upload-%, also searching through PREBUILDS for possible dependencies, and depending on dep-subupload to build them. - -dep-subupload does no dependency search, but has an identical docker push recipe to upload, and dep-upload. - -create-%, depend-create-%, and depend-subcreate-% work similarly to the upload rules, calling docker build instead of a docker push, and depending on the Dockerfile having been created. When depending on the Dockerfile, we look through the NOMANIFEST list, and insert "NOMANIFEST-" in the name of dependency on the dockerfile. This is so that we depend on the NOMANIFEST variant if the image we are building requires us to use a postfix on the image name to access a version for a specified architecture. otherwise, we run the Dockerfile-% rule that uses a prefix (i386/, amd64/, etc) to access the docker image we are building from. - -It's worth noting that for all of these *create* and *upload* rules, we pipe the output of docker to cat, which causes docker to stop trying to draw progress bars. This seriously cleans up the - - -#### Building Dockerfiles. - -There are two rules for creating Dockerfiles, and we decide in the *create* rules which of these to use by looking at the NOMANIFEST variable, and adding -NOMANIFEST in the name of the rule we depend on for dockerfile creation. - -The rules are relatively simple: -```bash -$ cat Makefile | sed -n -E '/^(Dock)/{:n;N;s/\n\t/\n /;tn;p}' -Dockerfile-NOMANIFEST-%: $$(call snd,%)/Dockerfile - cd $(call snd,$*) && cat Dockerfile | ${SED} "s/^\(MAINTAINER\).*/\1 $(REALNAME) \"$(EMAIL)\"/" | ${SED} "s=^\(FROM \)\(.*\)$$=\1\2$(call archpostfix,$*)=" > Dockerfile-$(call fst,$*) - -Dockerfile-%: $$(call snd,%)/Dockerfile - cd $(call snd,$*) && cat Dockerfile | ${SED} "s/^\(MAINTAINER\).*/\1 $(REALNAME) \"$(EMAIL)\"/" | ${SED} "s=^\(FROM \)\(.*\)$$=\1$(call archpath,$(call fst,$*))\2=" > Dockerfile-$(call fst,$*) -$ -``` - -These two rules depend on the checkout of the git repos containing the Dockerfiles. they do this by depending on /Dockerfile. The rules are responsible for the creation of individual architecture specific derivitives of the Dockerfile that is downloaded. additionally, the rules set the MAINTAINER of the docker image to be us. Most of the heavy lifting of these rules is being done in the archpostfix, and archpath functions, which are being used in a sed expression to either postfix or prefix the image that this image is built from. - - -Let's take a look at that sed with a simpler example: - -SED ABUSE: -```bash -$ echo "FROM image-version" | sed "s=^\(FROM \)\(.*\)$=\1i386/\2=" -FROM i386/image-version -$ -``` - -Unlike our previous sed commands, which have all been forms of "look for this thing, and display it", with the 's' command basically being abused as a test, this one intentionally is making a change. - -'s' commands are immediately followed by a character, that is used to seperate and terminate two blocks of text: the part we're looking for (match section), and the part we're replacing it with(substitution section). Previously, we've used '/' as the character following a 's' command, but since we're using '/' in the text we're placing into the file, we're going to use the '=' character instead. We've covered the '^' character at the beginning of the pattern being an anchor for "this pattern should be found only at the begining of the line". In the match section of this command, we're introducing "$" as the opposite anchor: $ means "end of line.". we're not using a -E on the command line, so are forced to use "\" before our parenthesis for our matching functions. this is a pure stylistic decision. the .* in the second matching section stands for 'any character, any number of times', which will definately match against our dependent image name. - -The match section of this sed command basicaly translates to "at the beginning of the line, look for "FROM ", store it, and store anything else you find up to the end of the line.". These two store operations get placed in sed variables, named \1, and \2. a SED command can have up to nine variables, which we are using in the substitution section. - -The substitution section of this sed command uses the \1 and \2 variable references to wrap the string "i386/". this effectively places i386/ in front of the image name. - -Because we are using that sed command in a Makefile, we have to double up the "$" symbol, to prevent make from interpreting it as a variable. In the first sed command in these rules, we're also doing some minor escaping, adding a '\' in front of some quotes, so that our substitution of the maintainer has quotes around the email address. - -#### Downloading Dockerfiles - -Finally, we are at the bottom of our dependency tree. We've followed this is reverse order, but when we actually ask for things to be pushed, or to be built, these rules are the first ones run. - -There are a lot of these, of various complexities, so let's start with the simple ones first. - -##### Simple Checkout - -```bash -$ cat Makefile | sed -n -E '/^(smtp|dynamo|minio)/{:n;N;s/\n\t/\n /;tn;p}' -smtp/Dockerfile: - git clone https://github.com/namshi/docker-smtp.git smtp - cd smtp && git reset --hard $(SMTP_COMMIT) - -dynamodb_local/Dockerfile: - git clone https://github.com/cnadiminti/docker-dynamodb-local.git dynamodb_local - cd dynamodb_local && git reset --hard $(DYNAMODB_COMMIT) - -minio/Dockerfile: - git clone https://github.com/minio/minio.git minio - cd minio && git reset --hard $(MINIO_COMMIT) - -``` - -These rules are simple. They git clone a repo, then reset the repo to a known good revision. This isolates us from potential breakage from upstreams, and prevents someone from stealing git credentials for our upstreams, and using those credentials to make a malignant version. - -##### Checkout with Modifications - -A bit more complex rule is localstack/Dockerfile: -```bash -$ cat Makefile | sed -n -E '/^(localsta)/{:n;N;s/\n\t/\n /;tn;p}' -localstack/Dockerfile: - git clone https://github.com/localstack/localstack.git localstack - cd localstack && git reset --hard $(LOCALSTACK_COMMIT) - ${SED} -i.bak "s=localstack/java-maven-node-python=$(USERNAME)/java_maven_node_python$(TAGNAME)=" $@ - # skip tests. they take too long. - ${SED} -i.bak "s=make lint.*=make lint=" localstack/Makefile - ${SED} -i.bak "s=\(.*lambda.*\)=#\1=" localstack/Makefile - -$ -``` - -This rule makes some minor modifications to localstack's Dockerfile, and the Makefile that localstack's build process places in the docker image. It changes the Dockerfile such that instead of depending on upstream's version of the java-maven-node-python docker image, we depend on the version we are building. additionally, we disable the test cases for localstack, because they take a long time, and have a timing issues on emulators. It's worth noting that we use the make "$@" variable here, which evaluates to the build target, AKA, everything to the left of the ":" on the first line of our rule. - -SED ABUSE: -These have a little bit of new sed, for us. We're using the '-i' option to sed, to perform sed operations in place, which is to say, we tell sed to edit the file, and store a backup of the file before it edited it as .bak. Other than that, these are standard substitutions, like we covered in our previous SED ABUSE section. - -In the same approximate category is the java_maven_node_python/Dockerfile rule: -```bash -$ cat Makefile | sed -n -E '/^(java)/{:n;N;s/\n\t/\n /;tn;p}' -java_maven_node_python/Dockerfile: - git clone https://github.com/localstack/localstack.git java_maven_node_python - cd java_maven_node_python && git reset --hard $(JAVAMAVENNODEPYTHON_COMMIT) - cd java_maven_node_python && mv bin/Dockerfile.base Dockerfile - # disable installing docker-ce. not available on many architectures in binary form. - ${SED} -i.bak "/.*install Docker.*/{N;N;N;N;N;d}" $@ -``` - -This rule does a checkout like the localstack rule, but the Dockerfile is stored somewhere other that the root of the repo. we move the Dockerfile, then we disable the installation of docker-ce in the environment. we don't use it, and it's got problems with not being ported to all architectures. - -SED ABUSE: -To disable the installation of docker here, we do something a bit hacky. we find the line with 'install Docker' on it, we pull the next 5 lines into the pattern buffer, then delete them. This is effectively just a multiline delete. we use the -i.bak command line, just like the last sed abuse. neat and simple. - - -##### Checkout, Copy, Modify - -Some of the git repositories that we depend on do not store the Dockerfile in the root of the repository. instead, they have one large repository, with many directories containing many docker images. In these cases, we use git to check out the repository into a directory with the name of the image followed by '-all', then copy the directory we want out of the tree. - -```bash -$ cat Makefile | sed -n -E '/^(airdock)/{:n;N;s/\n\t/\n /;tn;p}' -airdock_base/Dockerfile: - git clone https://github.com/airdock-io/docker-base.git airdock_base-all - cd airdock_base-all && git reset --hard $(AIRDOCKBASE_COMMIT) - cp -R airdock_base-all/jessie airdock_base - # work around go compiler bug by using newer version of GOSU. https://bugs.launchpad.net/qemu/+bug/1696353 - ${SED} -i.bak "s/GOSU_VERSION=.* /GOSU_VERSION=1.11 /" $@ - # work around missing architecture specific binaries in earlier versions of tini. - ${SED} -i.bak "s/TINI_VERSION=.*/TINI_VERSION=v0.16.1/" $@ - # work around the lack of architecture usage when downloading tini binaries. https://github.com/airdock-io/docker-base/issues/8 - ${SED} -i.bak 's/tini\(.asc\|\)"/tini-\$$dpkgArch\1"/' $@ - -airdock_rvm/Dockerfile: - git clone https://github.com/airdock-io/docker-rvm.git airdock_rvm-all - cd airdock_rvm-all && git reset --hard $(AIRDOCKRVM_COMMIT) - cp -R airdock_rvm-all/jessie-rvm airdock_rvm - ${SED} -i.bak "s=airdock/base:jessie=$(USERNAME)/airdock_base$(TAGNAME)=" $@ - # add a second key used to sign ruby to the dockerfile. https://github.com/airdock-io/docker-rvm/issues/1 - ${SED} -i.bak "s=\(409B6B1796C275462A1703113804BB82D39DC0E3\)=\1 7D2BAF1CF37B13E2069D6956105BD0E739499BDB=" $@ - -airdock_fakesqs/Dockerfile: - git clone https://github.com/airdock-io/docker-fake-sqs.git airdock_fakesqs-all - cd airdock_fakesqs-all && git reset --hard $(AIRDOCKFAKESQS_COMMIT) - cp -R airdock_fakesqs-all/0.3.1 airdock_fakesqs - ${SED} -i.bak "s=airdock/rvm:latest=$(USERNAME)/airdock_rvm$(TAGNAME)=" $@ - # add a workdir declaration to the final switch to root. - ${SED} -i.bak "s=^USER root=USER root\nWORKDIR /=" $@ - # break directory creation into two pieces, one run by root. - ${SED} -i.bak "s=^USER ruby=USER root=" $@ - ${SED} -i.bak "s=cd /srv/ruby/fake-sqs.*=chown ruby.ruby /srv/ruby/fake-sqs\nUSER ruby\nWORKDIR /srv/ruby/fake-sqs\nRUN cd /srv/ruby/fake-sqs \&\& \\\\=" $@ -``` - -In airdock_base/Dockefile, we do a clone, set it to the revision we are expecting, then copy out one directory from that repo, creating an airdock_base/ directory containing a Dockerfile, like we expect. We then change out some version numbers in the Dockerfile to work around some known bugs, and do a minor modification to two commands to allow airdock_base to be built for non-amd64 architectures. - -SED ABUSE: -The sed in the airdock_base/Dockerfile rule is relatively standard fare for us now, with the exception of the last command. in it, we use a match against "\(.asc\|\)", meaning either a .asc, or empty string. This lets this sed command modify both the line that contains the path to the signature for tini, and the path to the tini package. Since we want a '$' in the dockerfile, so that when the dockerfile is run, it looks at it's internal '$dpkgArch' variable, we have to escape it with a $ to prevent make from eating it, and with a \ to prevent SED from trying to interpret it. - -In airdock_rvm/Dockerfile, we do the same clone, reset hard, copy routine as we did in airdock_base/Dockerfile. Since airdock_rvm depends on airdock_base, we change the image this image derives from to point to our airdock_base image. Additionally, to work around the image using an old signature to verify it's ruby download, we add another key to the gpg import line in the Dockerfile. Technically both keys are in use by the project now, so we did not remove the old one. - -airdock_fakesqs had a bit more modification that was required. we follow the same routine as in airdock_rvm/Dockerfile, doing our clone, reset, copy, and dependant image change, then we have to make some modifications to the WORKDIR and USERs in this Dockerfile. I don't know how they successfully build it, but it looks to me like their original file is using a different docker file interpreter, with a different permissions model. when we tried to run the Dockerfile, it would give us permissions errors. These changes make it function, by being a bit more explicit about creating things with the right permissions. - -SED ABUSE: -Let's take a look at the effect of these sed commands, before we dig into the commands themselves. - -```bash -$ diff -u airdock_fakesqs-all/0.3.1/Dockerfile airdock_fakesqs/Dockerfile ---- airdock_fakesqs-all/0.3.1/Dockerfile 2019-03-11 16:47:40.367319559 +0000 -+++ airdock_fakesqs/Dockerfile 2019-03-11 16:47:40.419320902 +0000 -@@ -4,15 +4,19 @@ - # TO_BUILD: docker build --rm -t airdock/fake-sqs . - # SOURCE: https://github.com/airdock-io/docker-fake-sqs - --FROM airdock/rvm:latest -+FROM julialongtin/airdock_rvm:0.0.9 - MAINTAINER Jerome Guibert - ARG FAKE_SQS_VERSION=0.3.1 --USER ruby -+USER root - --RUN mkdir -p /srv/ruby/fake-sqs && cd /srv/ruby/fake-sqs && \ -+RUN mkdir -p /srv/ruby/fake-sqs && chown ruby.ruby /srv/ruby/fake-sqs -+USER ruby -+WORKDIR /srv/ruby/fake-sqs -+RUN cd /srv/ruby/fake-sqs && \ - rvm ruby-2.3 do gem install fake_sqs -v ${FAKE_SQS_VERSION} --no-ri --no-rdoc - - USER root -+WORKDIR / - - EXPOSE 4568 -``` - -The first change is our path change, to use the airdock_rvm image we're managing, instead of upstream's latest. -The second and third change happens at the place in this file where it fails. On my machine, the mkdir fails, as the ruby user cannot create this directory. to solve this, we perform the directory creation an root, THEN do our rvm work. - -Now, let's look through the sed that did that. -The first sed command in this rule changed the path on the FROM line, just like the similar sed statement in the last make rule we were looking at. -The second sed command added a 'WORKDIR /' to the bottom of the Dockerfile, after the USER root. -The third SED command changes the USER line at the top of the file to using the root user to run the next command, instead of the ruby user. -Finally, the fourth SED command changes the first RUN command into two run commands. one creates the directory and makes sure we have permissions to it, while the second runs our command. the sed command also inserts commands to change user to ruby, and change working directories to the directory created in the first RUN command. - -Structurally, the first, second, and third sed command are all pretty standard things we've seen before. The fourth command looks a little different, but really, it's the same sort of substitution, only it adds several lines. At the end of the statement is some tricky escaping. -'&' characters must be escaped, because in sed, an '&' character is shorthand for 'the entire patern that we matched'. That will be important, later. the single '\' character has to be escaped into '\\\\'. - -Note that when we wrote our 'clean' rule, we added these '-all' directories manually, to make sure they would get deleted. - -##### Checkout, Copy, Modify Multiline - -elasticsearch and cassandra's checkouts are complicated, as they do a bit of injection of code into the docker entrypoint script. The entrypoint script is the script that is launched when you run a docker image. It's responsible for reading in environment variables, setting up the service that the docker image is supposed to run, and then running the service. For both elasticsearch and cassandra, we do a multiline insert, and we do it with multiple chained commands. - -Let's look at elasticsearch, as these two rules are almost identical. - -```bash -$ cat Makefile | sed -n -E '/^(ela)/{:n;N;s/\n\t/\n /;tn;p}' -elasticsearch/Dockerfile: - git clone https://github.com/blacktop/docker-elasticsearch-alpine.git elasticsearch-all - cd elasticsearch-all && git reset --hard $(ELASTICSEARCH_COMMIT) - cp -R elasticsearch-all/5.6/ elasticsearch - # add a block to the entrypoint script to interpret CS_JVM_OPTIONS, modifying the jvm.options before launching elasticsearch. - # first, add a marker to be replaced before the last if. - ${SED} -i.bak -r ':a;$$!{N;ba};s/^(.*)(\n?)fi/\2\1fi\nREPLACEME/' elasticsearch/elastic-entrypoint.sh - # next, load our variables. - ${SED} -i.bak 's@REPLACEME@MY_APP_CONFIG="/usr/share/elasticsearch/config/"\n&@' elasticsearch/elastic-entrypoint.sh - # add our parser and replacer. - ${SED} -i.bak $$'s@REPLACEME@if [ ! -z "$${JVM_OPTIONS_ES}" ]; then\\nfor x in $${JVM_OPTIONS_ES}; do { l="$${x%%=*}"; r=""; e=""; [ "$$x" != "$${x/=//}" ] \&\& e="=" \&\& r="$${x##*=}"; [ "$$x" != "$${x##-Xm?}" ] \&\& r="$${x##-Xm?}" \&\& l="$${x%%$$r}"; echo $$l $$e $$r; sed -i.bak -r \'s/^[# ]?(\'"$$l$$e"\').*/\\\\1\'"$$r"\'/\' "$$MY_APP_CONFIG/jvm.options"; diff "$$MY_APP_CONFIG/jvm.options.bak" "$$MY_APP_CONFIG/jvm.options" \&\& echo "no difference"; } done;\\nfi\\n&@' elasticsearch/elastic-entrypoint.sh - # remove the marker we added earlier. - ${SED} -i.bak 's@REPLACEME@@' elasticsearch/elastic-entrypoint.sh - -$ -``` - -In this rule, we're checking out the git tree, and copying one directory that contains our Dockerfile, and our entrypoint for elasticsearch. Following that, we have four sed commands, one of which inserts some very complicated bash. - -SED ABUSE: -Our first sed command in this rule uses a new trick. We're using -i to edit in place, and -r to quash output. Instead of starting with a match(/.../) or a substitution (s/thing/otherthing/), we immediately start with a label. let's break down this command. - -```sed -:a; # an anchor, we can loop back to. -$!{ # enter here only if there is content to be read from the file. note that to get this "$" past make, we had to escape it, by replacing it with $$. -N; # pull in the next line of content into the pattern space -ba # branch to the 'a' label. -}; -s/(.*)(\n?)fi/\2\1fi\nREPLACEME/ #match everything up to the last 'fi' and replace it with a 'fi', a new line, and REPLACEME -``` - -What does that effectively do? the source file contains a lot of lines with 'fi' in them, by inserting REPLACEME after the last one, this gives us an anchor point, that we can safely run simpler sed commands against. - -for instance, our next sed command: -```sed -s@REPLACEME@MY_APP_CONFIG="/usr/share/elasticsearch/config/"\n&@ -``` - -the 's' on this command is using '@' symbols to seperate the pattern from the replacement. it operates by finding the 'REPLACEME' that we inserted with the last command. As we touched on earlier, the unescaped '&' at the end of this replacement repeats back the patern, in the replacement. This effectively means that this line replaces REPLACEME with a new line of code, and puts the REPLACEME after the line it inserted. - -BASH ABUSE: -The next sed command works similarly, however it inserts an extremely complicated pile of bash on one line. Let's take a look at it. I'm going to remove some semicolons, remove some of the escaping, and insert line breaks and comments, to make this a bit more readable. -```bash -if [ ! -z "$${JVM_OPTIONS_ES}" ]; then # only if JVM_OPTIONS_ES was set when docker was run - for x in $${JVM_OPTIONS_ES} - do { - # set l to everything to the left of an equal sign. - l="${x%%=*}" - # clear out r and e. - r="" - e="" - # if there was an equal sign, set e to an equal sign, and set r to everything after the equal sign. - [ "$x" != "${x/=//}" ] && e="=" && r="$${x##*=}" - # if there was an '-Xm' (a java memory option), set r to the content after the (-XM), and set l to the -XM - [ "$x" != "${x##-Xm?}" ] && r="$${x##-Xm?}" && l="${x%%$r}" - # debugging code. echo what we saw. - echo $l $e $r - # perform a substitution, uncommenting a line found that starts with $l$e, and replacing it with $l$e$r. - sed -i.bak -r 's/^[# ]?('"$l$e"').*/\1'"$r"'/' "$MY_APP_CONFIG/jvm.options" - # show that a change was done with diff, or say there was no difference. - diff "$$MY_APP_CONFIG/jvm.options.bak" "$MY_APP_CONFIG/jvm.options" && echo "no difference"; - } done; -fi -``` - -What this bash script is doing is, it looks for a JVM_OPTIONS_ES environment variable, and if it finds it, it rewrites the jvm.options file, uncommenting and replacing the values for java options. This allows us to change the memory pool settings, and possibly other settings, by setting a variable in the docker compose file that starts up our integration test. - -This bash script is inserted by a sed command and CONTAINS a sed command, and lots of special characters. The quoting of this is handled a bit differently: instead of just surrounding our sed command in '' characters, we use $'', which is bash for "use C style escaping here". - -SED ABUSE: -the bash script above uses a relatively normal sed command, but intersparces it with ' and " characters, in order to pass the sed command in groups of '' characters, while using "" around the sections that we have variables in. bash will substitute variables in doublequotes, but will not substitute them in single quotes. -This substitution command uses slashes as its separators. it starts by anchoring to the beginning of the line, and matching against either a single '#' character, or a single space. it does this by grouping the space and # in a character class ([ and ]), then using a question mark to indicate "maybe one of these.". the substitution continues by matching the bash variables $l and $e, saving them in \1, matching (and therefore removing) anything else on the line, and replacing the line with \1, followed immediately by the contents of the bash variable $r. - -The cassandra/Dockerfile rule is almost identical to this last rule, only substituting out the name of the variable we expect from docker to CS_JVM_OPTIONS, and changing the path to the jvm.options file. - -# Pitfalls I fell into writing this. - -The first large mistake I made when writing this, is that the root of the makefile's dependency tree contained both images that had dependencies, and the dependent images themselves. This had me writing methods to keep the image build process from stepping on itsself. what was happening is that, in the case of the airdock-* and localstack images, when trying to build all of the images at once, make would race all the way down to the git clone steps, and run the git clone multiple times at the same time, where it just needs to be run once. - -The second was that I didn't really understand that manifest files refer to dockerhub only, not to the local machine. This was giving me similar race conditions, where an image build for architecture A would complete, and try to build the manifest when architecture B was still building. - -The third was writing really complicated SED and BASH and MAKE. ;p \ No newline at end of file +file has moved [here](../legacy/reference/make-docker-and-qemu.md) diff --git a/docs/reference/provisioning/scim-token.md b/docs/reference/provisioning/scim-token.md index b7ae891de53..0128216c9d9 100644 --- a/docs/reference/provisioning/scim-token.md +++ b/docs/reference/provisioning/scim-token.md @@ -1,114 +1 @@ -# SCIM tokens {#RefScimToken} - -_Author: Artyom Kazak_ - ---- - -A _SCIM token_ is a bearer token used to authorize SCIM operations. - -A team owner can create SCIM tokens for the team. Each of those tokens can be used to provision new members to the team, modify members' profile attributes, etc. Tokens have unlimited duration, but can be revoked. - -## Using a SCIM token {#RefScimTokenUsage} - -SCIM tokens are not general-purpose API tokens. They only apply to the `/scim/v2/` subtree of the API. - -SCIM tokens are intended to be used by provisioning tools, such as Okta, OneLogin, Active Directory, and so on. If you have your own provisioning utility, you can use a token by adding an `Authorization` header to all `/scim/v2/` requests: - -``` -Authorization: Bearer -``` - -A SCIM token identifies the team that the token belongs to, so you do not need to specify the team in the request. - -## API {#RefScimTokenApi} - -### Creating a token {#RefScimTokenCreate} - -Creating a token requires the user to be a team owner. As an additional precaution, we also require the user to re-enter their password. - -There is a reasonable limit on the number of tokens a single team can have, set in `scim.yaml` at `maxScimTokens`. For Wire the limit is 16. - -Sample request and response: - -``` -POST /scim/auth-tokens - -{ - // Token description (useful as a memory aid; non-optional but can be empty) - "description": "Sample token", - - // User password. If the user does not have a password (e.g. they are - // SCIM-provisioned), this field should be omitted. - "password": "secret" -} -``` - -``` -201 Created - -{ - // Token itself; only sent once, can not be listed - "token": "MzIgcmFuZG9tIGJ5dGVzIGluIGJhc2U2NCBmb3JtYXQ=", - - // Metadata about the token, can be listed - "info": { - // Token ID, can be used to revoke the token - "id": "514613de-9fde-4aab-a704-c57af7a3366e", - // Token description - "description": "Sample token", - // Team associated with the token - "team": "78c65523-490f-4e45-881f-745e3458e280", - // When the token was created - "created_at": "2019-04-18T14:09:43.732Z", - // Identity provider for users provisioned with the token - // (optional but for now always present) - "idp": "784edde5-29d8-4dc3-bb95-924519448f09" - } -} -``` - -Note that SCIM can only be used with teams that have either no or exactly one SAML IdP ([internal issue](https://github.com/zinfra/backend-issues/issues/1377)). - -### Listing existing tokens {#RefScimTokenList} - -Listing tokens requires the user to be a team owner. - -We don't ever send tokens themselves, only the metadata (which can be used, for instance, to decide which tokens to revoke). - -Sample request and response: - -``` -GET /scim/auth-tokens -``` - -``` -200 OK - -{ - "tokens": [ - { - "id": "514613de-9fde-4aab-a704-c57af7a3366e", - "description": "Sample token", - "team": "78c65523-490f-4e45-881f-745e3458e280", - "created_at": "2019-04-18T14:09:43.732Z", - "idp": "784edde5-29d8-4dc3-bb95-924519448f09" - } - ] -} -``` - -### Revoking a token {#RefScimTokenDelete} - -Revoking a token requires the user to be a team owner. - -To revoke a token, the user has to provide the token ID (not the token itself). The revoked token becomes unused immediately and does not show up in the results of `GET /scim/auth-tokens`. - -Sample request and response: - -``` -DELETE /scim/auth-tokens?id=514613de-9fde-4aab-a704-c57af7a3366e -``` - -``` -204 No Content -``` +file has moved [here](../legacy/reference/provisioning/scim-token.md) diff --git a/docs/reference/provisioning/scim-via-curl.md b/docs/reference/provisioning/scim-via-curl.md index aaa2a9eea56..d34fd5ade02 100644 --- a/docs/reference/provisioning/scim-via-curl.md +++ b/docs/reference/provisioning/scim-via-curl.md @@ -1 +1 @@ -# This page [has gone here](https://docs.wire.com/understand/single-sign-on/main.html#using-scim-via-curl). +file has moved [here](../legacy/reference/provisioning/scim-via-curl.md) diff --git a/docs/reference/provisioning/wire_scim_token.py b/docs/reference/provisioning/wire_scim_token.py deleted file mode 100755 index 2823a7bbca8..00000000000 --- a/docs/reference/provisioning/wire_scim_token.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python3 -# -# This file is part of the Wire Server implementation. -# -# Copyright (C) 2020 Wire Swiss GmbH -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU Affero General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) any -# later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License along -# with this program. If not, see . - -from __future__ import print_function - -# NOTE: This python script requires the "requests" library to be installed. - -# Change this if you are running your own instance of Wire. -BACKEND_URL='https://prod-nginz-https.wire.com' - -import sys -import getpass -from requests import Request, Session -import requests -import json -import datetime - -session = None - -def init_session(): - global session - session = Session() - session.headers.update({'User-Agent': "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"}) - -def post(url): - return Request('POST', url) - -def get(url): - return Request('GET', url) - -def set_bearer_token(request, token): - request.headers['Authorization'] = 'Bearer ' + token - -def backend(path): - return BACKEND_URL + path - -def has_json_content(response): - content_type = response.headers.get('Content-Type') - if content_type is not None: - return (content_type.startswith('application/json') - or content_type == 'application/scim+json;charset=utf-8') - else: - return False - -def send_request(session, request): - response = session.send(session.prepare_request(request)) - if 200 <= response.status_code and response.status_code < 300: - if has_json_content(response): - return response.json() - else: - return response - else: - print(f"Request failed {request.url}", file=sys.stderr) - if has_json_content(response): - tpl = response, response.json() - else: - tpl = response, response.content - print(tpl, file=sys.stderr) - exit(1) - -def create_bearer(email, password): - r = post(backend('/login?persist=false')) - r.headers['Accept'] = 'application/json' - r.json = {'email': email, 'password': password} - return send_request(session, r) - -def create_scim_token(admin_password, token): - r = post(backend('/scim/auth-tokens')) - set_bearer_token(r, token) - r.json = {'description': 'token generated at ' + datetime.datetime.now().isoformat(), - 'password': admin_password - } - return send_request(session, r) - -def exit_fail(msg): - print(msg, file=sys.stderr) - exit(1) - -def main(): - init_session() - print('This script generates a token that authorizes calls to Wire\'s SCIM endpoints.\n') - print('Please enter the login credentials of a user that has role "owner" or "admin".') - ADMIN_EMAIL=input("Email: ") or exit_fail('Please provide an email.') - ADMIN_PASSWORD=getpass.getpass('Password: ') or exit_fail('Please provide password.') - bearer_token = create_bearer(ADMIN_EMAIL, ADMIN_PASSWORD) - scim_token = create_scim_token(ADMIN_PASSWORD, bearer_token['access_token']) - print('Wire SCIM Token: ' + scim_token['token']) - print('The token will be valid until you generate a new token for this user.') - -if __name__ == '__main__': - main() diff --git a/docs/reference/provisioning/wire_scim_token.py b/docs/reference/provisioning/wire_scim_token.py new file mode 120000 index 00000000000..7be5401a613 --- /dev/null +++ b/docs/reference/provisioning/wire_scim_token.py @@ -0,0 +1 @@ +../../legacy/reference/provisioning/wire_scim_token.py \ No newline at end of file diff --git a/docs/reference/spar-braindump.md b/docs/reference/spar-braindump.md index f92775becaf..d3e2b1f3ffd 100644 --- a/docs/reference/spar-braindump.md +++ b/docs/reference/spar-braindump.md @@ -1,405 +1 @@ -# Spar braindump {#SparBrainDump} - -_Author: Matthias Fischmann_ - ---- - -# the spar service for user provisioning (scim) and authentication (saml) - a brain dump - -this is a mix of information on inmplementation details, architecture, -and operation. it should probably be sorted into different places in -the future, but if you can't find any more well-structured -documentation answering your questions, look here! - - -## related documentation - -- [list of howtos for supported SAML IdP vendors](https://docs.wire.com/how-to/single-sign-on/index.html) -- [fragment](https://docs.wire.com/understand/single-sign-on/design.html) (TODO: clean up the section "common misconceptions" below and move it here.) -- [official docs for team admin from customer support](https://support.wire.com/hc/en-us/categories/360000248538?section=administration%3Fsection%3Dadministration) (skip to "Authentication") -- [talk scim using curl](https://github.com/wireapp/wire-server/blob/develop/docs/reference/provisioning/scim-via-curl.md) -- if you want to work on our saml/scim implementation and do not have access to [https://github.com/zinfra/backend-issues/issues?q=is%3Aissue+is%3Aopen+label%3Aspar] and [https://github.com/wireapp/design-specs/tree/master/Single%20Sign%20On], please get in touch with us. - - -## design considerations - -### SCIM without SAML. - -Before https://github.com/wireapp/wire-server/pull/1200, scim tokens could only be added to teams that already had exactly one SAML IdP. Now, we also allow SAML-less teams to have SCIM provisioning. This is an alternative to onboarding via team-settings and produces user accounts that are authenticated with email and password. (Phone may or may not work, but is not officially supported.) - -The way this works is different from team-settings: we don't send invites, but we create active users immediately the moment the SCIM user post is processed. The new thing is that the created user has neither email nor phone nor a SAML identity, nor a password. - -How does this work? - -**email:** If no SAML IdP is present, SCIM user posts must contain an externalId that is an email address. This email address is not added to the newly created user, because it has not been validated. Instead, the flow for changing an email address is triggered in brig: an email is sent to the address containing a validation key, and once the user completes the flow, brig will add the email address to the user. We had to add very little code for this in this PR, it's all an old feature. - -When SCIM user gets are processed, in order to reconstruct the externalId from the user spar is retrieving from brig, we introduce a new json object for the `sso_id` field that looks like this: `{'scim_external_id': 'me@example.com'}`. - -In order to find users that have email addresses pending validation, we introduce a new table in spar's cassandra called `scim_external_ids`, in analogy to `user`. We have tried to use brig's internal `GET /i/user&email=...`, but that also finds pending email addresses, and there are corner cases when changing email addresses and waiting for the new address to be validated and the old to be removed... that made this approach seem infeasible. - -**password:** once the user has validated their email address, they need to trigger the "forgot password" flow -- also old code. - - -## operations - -### enabling / disabling the sso feature for a team - -if you have sso disabled by default, you need to turn on the feature -for every team that wants to use it. you can do this in the stern -service (aka backoffice). look for `get/put -/teams/{tid}/features/sso` - - -### registering an idp with a team via curl - -you need to have: - -```sh -# user id of an admin of the team (or the creator from the team info -# in the backoffice, if you only have the team id). -export ADMIN_ID=... - -# path of the xml metadata file (if you only have the url, curl it) -export METADATA_FILE=... -``` - -copy these two files to one of your spar instances: - -- `.../wire-server/deploy/services-demo/register_idp_internal.sh` -- `${METADATA_FILE}` - -... and ssh into it. then: - -```sh -./register_idp_internal.sh metadata.xml ${TEAM_OWNER_ID} -``` - -the output contains the a json object representing the idp. construct -the login code from the `id` field of that object by adding `wire-` in -front, eg.: - -``` -wire-e97fbe2e-eeb1-11e9-acf3-9ba77d8a04bf -``` - -give this login code to the users that you want to connect to wire -using this idp. see -[here](https://support.wire.com/hc/en-us/articles/360000954617-Login-with-SSO) -on how to use the login code. - - -### updating an idp via curl - -Your IdP metadata may change over time (eg., because a certificate -expires, or because you change vendor and all the metadata looks -completely different), these changes need to be updated in wire. We -offer two options to do that. - -Like when creating your first IdP, for both options you need define a -few things: - -``` -# user id of an admin of the team (or the creator from the team info -# in the backoffice, if you only have the team id). -export ADMIN_ID=... - -# path of the xml metadata file (if you only have the url, curl it) -export METADATA_FILE=... - -# The ID of the IdP you want to update (login code without the `wire-` -# prefix, which is a UUIDv4): -export IDP_ID=... -``` - -Copy the new metadata file to one of your spar instances. - -Ssh into it. If you can't, [the scim -docs](provisioning/scim-via-curl.md) explain how you can create a -bearer token if you have the admin's login credentials. If you follow -that approach, you need to replace all mentions of `-H'Z-User ...'` -with `-H'Authorization: Bearer ...'` in the following, and you won't need -`$ADMIN_ID`, but something like `$BEARER`. - -There are two ways to update an IDP, described below, each with their own tradeoffs that affect users. - -#### Option 1: Update the existing IdP in-place - -Effects: - -- You keep the login code, no visible changes for your users. -- The old IdP becomes immediately unaccessible. It will disappear - from team-settings, and users will have no way of using it for - authentication. -- If the has no account on the new IdP, she won't be able to login. -- If a user has an account on the new IdP, *but not with exactly the - same user name* (SAML NameID), she will not be logged in to their - old account. Instead, depending on your setup, a second account is - created for them, or they are blocked (both not what you want). - -```shell -curl -v \ - -XPUT http://localhost:8080/identity-providers/${IDP_ID} \ - -H"Z-User: ${ADMIN_ID}" \ - -H'Content-type: application/xml' \ - -d@"${METADATA_FILE}" -``` - -#### Option 2: Create a second IdP, and mark it as replacing the old one. - -Effects: - -- The new IdP will have a new login code. Users need to be invited to - use this new login code. -- If they use the old login code, they can keep using the old IdP as - long as it is still running as before. (This option is good for - smooth transitions from one IdP to the other.) -- If they use the new login code, they will be automatically moved - from the old IdP to the new one. After that, they won't be able to - use the old one any more. -- If a user logs into the team for the first time using the old login - code, no user will be created. The old IdP is marked as "replaced", - and wire only authenticates existing users with it. -- This doesn't currently work if you are using SCIM for provisioning, - because SCIM requires you to have exactly one IdP configured in your - wire team. (Internal details: - https://github.com/zinfra/backend-issues/issues/1365, - https://github.com/zinfra/backend-issues/issues/1377). -- If you go to team settings, you will see the old IdP and the new - one, and there is currently no way to distinguish between replaced - and active IdPs. (Internal details: - https://github.com/wireapp/wire-team-settings/issues/3465). - -```shell -curl -v \ - -XPOST http://localhost:8080/identity-providers'?replaces='${IDP_ID} \ - -H"Z-User: ${ADMIN_ID}" \ - -H'Content-type: application/xml' \ - -d@"${METADATA_FILE}" -``` - - -### deleting an idp via curl - -Read the beginning of the last section up to "Option 1". You need -`ADMIN_ID` (or `BEARER`) and `IDP_ID`, but not `METADATA_FILE`. - -```shell -curl -v - -XDELETE http://localhost:8080/identity-providers/${IDP_ID} \ - -H"Z-User: ${ADMIN_ID}" \ - -H'Content-type: application/json -``` - -If there are still users in your team with SAML credentials associated -with this IdP, you will get an error. You can either move these users -elsewhere, delete them manually, or purge them implicitly during -deletion of the IdP: - -```shell -curl -v - -XDELETE http://localhost:8080/identity-providers/${IDP_ID}?purge=true \ - -H"Z-User: ${ADMIN_ID}" \ - -H'Content-type: application/json -``` - -Haskell code: https://github.com/wireapp/wire-server/blob/d231550f67c117b7d100c7c8c6c01b5ad13b5a7e/services/spar/src/Spar/API.hs#L217-L271 - - -### setting a default SSO code - -To avoid having to give users the login code, a backend can also provide a default code on the endpoint `/sso/settings`. -This needs to be set explicitly, since this is not always wanted and there might even be multiple idps (each with their own login code): - -``` -curl -XPUT ${API_URL}/i/sso/settings -H 'Content-Type: application/json' -d '{"default_sso_code":"e97fbe2e-eeb1-11e9-acf3-9ba77d8a04bf"}' -``` - -Note the lack of the `wire-` prefix. - -This entry gets removed automatically when the corresponding idp is deleted. You can manually delete the default by making a `PUT` request as shown above with payload `{"default_sso_code":null}`. - -Clients can then ask for the default SSO code on `/sso/settings` and use it to initiate single sign-on. - - -### troubleshooting - -#### gathering information - -- find metadata for team in table `spar.idp_raw_metadata` via cqlsh - (since https://github.com/wireapp/wire-server/pull/872) - -- ask user for screenshots of the error message, or even better, for - the text. the error message contains lots of strings that you can - grep for in the spar sources. - - -#### making spar work with a new IdP - -often, new IdPs work out of the box, because there appears to be some -consensus about what minimum feature set everybody should support. - -if there are problems: collect the metadata xml and an authentication -response xml (either from the browser http logs via a more technically -savvy customer; FUTUREWORK: it would be nice to log all saml response -xml files that spar receives in prod and cannot process). - -https://github.com/wireapp/saml2-web-sso supports writing [unit vendor -compatibility -tests](https://github.com/wireapp/saml2-web-sso/blob/ff9b9f445475809d1fa31ef7f2932caa0ed31613/test/Test/SAML2/WebSSO/APISpec.hs#L266-L329) -against that response value. once that test passes, it should all -work fine. - - -### common misconceptions - - -#### an email address can be one of two things - -When users are SAML-authenticated with an email address under NameID, -that email address is used by wire as an opaque identifier, not to -send actual emails. In order to *also* assign the user that email -address, you can enable the feature flag `validateSAMLemails`. This -will trigger the regular email validation flow that is also triggered -when the user changes their email themselves. - - -#### scim, provisioning, metadata - -for changing the user information (name, handle, email, ...), saml -isn't enough. the identity management software (AD in this case, or -some add-on) needs to support scim. we *could* support doing that via -saml, but the part of the standards that are needed for that are even -in worse shape than the ones for the authentication bits, and it would -not lead to a good user experience. so instead we require users to -adopt the more robust and contemporary scim standard. - - -#### we don't support binding password/phone-auth'ed users to saml yet - -to keep track of whether we have, see https://github.com/zinfra/backend-issues/issues/731 - - - -## application logic - -### deleting users that exist on spar - -For scim- or saml-created users, there are three locations for user data: - -- `brig.user` (and a few things associated with that on brig and galley) -- `spar.user` -- `spar.scim_user` - -The single source of truth is `brig.user`. Dangling entries in the -other places are allowed, and must be checked by the application code -for danglingness. ([test case for -scim](https://github.com/wireapp/wire-server/blob/010ca7e460d13160b465de24dd3982a397f94c16/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs#L239-L308); -[test case for -saml](https://github.com/wireapp/wire-server/blob/293518655d7bae60fbcb0c4aaa06034785bfb6fc/services/spar/test-integration/Test/Spar/APISpec.hs#L742-L795)) - -For the semantics of interesting corner cases, consult [the test -suite](https://github.com/wireapp/wire-server/blob/develop/services/spar/test-integration/Test/Spar/APISpec.hs). -If you can't find what you're looking for there, please add at least a -pending test case explaining what's missing. - -Side note: Users in brig carry an enum type -[`ManagedBy`](https://github.com/wireapp/wire-server/blob/010ca7e460d13160b465de24dd3982a397f94c16/libs/brig-types/src/Brig/Types/Common.hs#L393-L413). This is a half-implemented feature for -managing conflicts between changes via scim vs. changes from wire -clients; and does currently not affect deletability of users. - - -#### delete via deleting idp - -[Currently](https://github.com/wireapp/wire-server/blob/d231550f67c117b7d100c7c8c6c01b5ad13b5a7e/services/spar/src/Spar/API.hs#L217-L271), we only have the rest API for this. Team settings will follow with a button. - - -#### user deletes herself - -TODO - - -#### delete in team settings - -TODO (probably little difference between this and "user deletes herself"?) - - -#### delete via scim - -TODO - - -## using the same IdP (same entityID, or Issuer) with different teams - -Some SAML IdP vendors do not allow to set up fresh entityIDs (issuers) -for fresh apps; instead, all apps controlled by the IdP are receiving -SAML credentials from the same issuer. - -In the past, wire has used the a tuple of IdP issuer and 'NameID' -(Haskell type 'UserRef') to uniquely identity users (tables -`spar.user_v2` and `spar.issuer_idp`). - -In order to allow one IdP to serve more than one team, this has been -changed: we now allow to identity an IdP by a combination of -entityID/issuer and wire `TeamId`. The necessary tweaks to the -protocol are listed here. - -For everybody using IdPs that do not have this limitation, we have -taken great care to not change the behavior. - - -### what you need to know when operating a team or an instance - -No instance-level configuration is required. - -If your IdP supports different entityID / issuer for different apps, -you don't need to change anything. We hope to deprecate the old -flavor of the SAML protocol eventually, but we will keep you posted in -the release notes, and give you time to react. - -If your IdP does not support different entityID / issuer for different -apps, keep reading. At the time of writing this section, there is no -support for multi-team IdP issuers in team-settings, so you have two -options: (1) use the rest API directly; or (2) contact our customer -support and send them the link to this section. - -If you feel up to calling the rest API, try the following: - -- Use the above end-point `GET /sso/metadata/:tid` with your `TeamId` - for pulling the SP metadata. -- When calling `POST /identity-provider`, make sure to add - `?api_version=v2`. (`?api_version=v1` or no omission of the query - param both invoke the old behavior.) - -NB: Neither version of the API allows you to provision a user with the -same Issuer and same NamdID. RATIONALE: this allows us to implement -'getSAMLUser' without adding 'TeamId' to 'UserRef', which in turn -would break the (admittedly leaky) abstarctions of saml2-web-sso. - - -### API changes in more detail - -- New query param `api_version=` for `POST - /identity-providers`. The version is stored in `spar.idp` together - with the rest of the IdP setup, and is used by `GET - /sso/initiate-login` (see below). -- `GET /sso/initiate-login` sends audience based on api_version stored - in `spar.idp`: for v1, the audience is `/sso/finalize-login`; for - v2, it's `/sso/finalize-login/:tid`. -- New end-point `POST /sso/finalize-login/:tid` that behaves - indistinguishable from `POST /sso/finalize-login`, except when more - than one IdP with the same issuer, but different teams are - registered. In that case, this end-point can process the - credentials by discriminating on the `TeamId`. -- `POST /sso/finalize-login/:tid` remains unchanged. -- New end-point `GET /sso/metadata/:tid` returns the same SP metadata as - `GET /sso/metadata`, with the exception that it lists - `"/sso/finalize-login/:tid"` as the path of the - `AssertionConsumerService` (rather than `"/sso/finalize-login"` as - before). -- `GET /sso/metadata` remains unchanged, and still returns the old SP - metadata, without the `TeamId` in the paths. - - -### database schema changes - -[V15](https://github.com/wireapp/wire-server/blob/b97439756cfe0721164934db1f80658b60de1e5e/services/spar/schema/src/V15.hs#L29-L43) +file has moved [here](../legacy/reference/spar-braindump.md) diff --git a/docs/reference/user/activation.md b/docs/reference/user/activation.md index 60868cd5811..63bf6c00f1c 100644 --- a/docs/reference/user/activation.md +++ b/docs/reference/user/activation.md @@ -1,152 +1 @@ -# Activation {#RefActivation} - -_Author: Artyom Kazak_ - ---- - -A user is called _activated_ they have a verified identity -- e.g. a phone number that has been verified via a text message, or an email address that has been verified by sending an activation code to it. - -A user that has been provisioned via single sign-on is always considered to be activated. - -## Activated vs. non-activated users {#RefActivationBenefits} - -Non-activated users can not [connect](connection.md) to others, nor can connection requests be made to anonymous accounts from verified accounts. As a result: - -* A non-activated user cannot add other users to conversations. The only way to participate in a conversation is to either create a new conversation with link access or to use a link provided by another user. - -The only flow where it makes sense for non-activated users to exist is the [wireless flow](registration.md#RefRegistrationWireless) used for [guest rooms](https://wire.com/en/features/encrypted-guest-rooms/) - -## API {#RefActivationApi} - -### Requesting an activation code {#RefActivationRequest} - -During the [standard registration flow](registration.md#RefRegistrationStandard), the user submits an email address or phone number by making a request to `POST /activate/send`. A six-digit activation code will be sent to that email address / phone number. Sample request and response: - -``` -POST /activate/send - -{ - // Either 'email' or 'phone' - "phone": "+1234567890" -} -``` - -``` -200 OK -``` - -The user can submit the activation code during registration to prove that they own the email address / phone number. - -The same `POST /activate/send` endpoint can be used to re-request an activation code. Please use this ability sparingly! To avoid unnecessary activation code requests, users should be warned that it might take up to a few minutes for an email or text message to arrive. - -### Activating an existing account {#RefActivationSubmit} - -If the account [has not been activated during verification](registration.md#RefRegistrationNoPreverification), it can be activated afterwards by submitting an activation code to `POST /activate`. Sample request and response: - -``` -POST /activate - -{ - // One of 'phone', 'email', or 'key' - "phone": "+1234567890", - - // 6-digit activation code - "code": "123456", - - // Verify the 'code' but don't activate the account (the 3-attempt limit - // on failed verification attempts still applies) - "dryrun": false -} -``` - -``` -200 OK - -{ - "phone": "+1234567890", - - // Whether it is the first successful activation for the user - "first": true -} -``` - -If the email or phone has been verified already, `POST /activate` will return status code `204 No Content`. If the code is invalid, `POST /activate` will return status code `404 Not Found` with `"label": "invalid-code"`. - -There is a maximum of 3 activation attempts per activation code. On the third failed attempt the code is invalidated and a new one must be requested. - -### Activation event {#RefActivationEvent} - -When the user becomes activated, they receive an event: - -```json -{ - "type": "user.activate", - "user": -} -``` - -### Detecting activation in the self profile {#RefActivationProfile} - -In addition to the [activation event](#RefActivationEvent), activation can be detected by polling the self profile: - -``` -GET /self - -{ - "accent_id": 0, - "assets": [], - "email": "pink@example.com", - "id": "2f7e582b-9d99-4d50-bbb0-e659d63491d9", - "locale": "en", - "managed_by": "wire", - "name": "Pink", - "picture": [] -} -``` - -If the profile includes `"email"` or `"phone"`, the account is activated. - -## Automating activation via email {#RefActivationEmailHeaders} - -Our email verification messages contain headers that can be used to automate the activation process. - -An email caused by `POST /activate/send` will contain this set of headers: - -``` -X-Zeta-Purpose: Verification -X-Zeta-Code: 123456 -``` - -An email caused by `POST /register` will contain this set of headers (the opaque `"key"` might be used instead of `"email"` in the `POST /activate` request): - -``` -X-Zeta-Purpose: Activation -X-Zeta-Key: ... -X-Zeta-Code: 123456 -``` - -## Phone/email whitelist {#RefActivationWhitelist} - -The backend can be configured to only allow specific phone numbers or email addresses to register. The following options have to be set in `brig.yaml`: - -```yaml -optSettings: - setWhitelist: - whitelistUrl: ... # Checker URL - whitelistUser: ... # Basic auth username - whitelistPass: ... # Basic auth password -``` - -When those options are present, the backend will do a GET request at `?email=...` or `?mobile=...` for every activation request it receives. It will expect either status code 200 ("everything good") or 404 ("provided email/phone is not on the whitelist"). - -If an email address or phone number are rejected by the whitelist, `POST /activate/send` or `POST /register` will return `403 Forbidden`: - -```json -{ - "code": 403, - "label": "unauthorized", - "message": "Unauthorized e-mail address or phone number." -} -``` - -Currently emails at `@wire.com` are always considered whitelisted, regardless of the whitelist service's response. +file has moved [here](../legacy/reference/user/activation.md) diff --git a/docs/reference/user/connection-transitions.png b/docs/reference/user/connection-transitions.png index 1df6b8e6813..6722ff359bf 100644 Binary files a/docs/reference/user/connection-transitions.png and b/docs/reference/user/connection-transitions.png differ diff --git a/docs/reference/user/connection-transitions.xml b/docs/reference/user/connection-transitions.xml index 1a3424e074a..b78f17675bd 100644 --- a/docs/reference/user/connection-transitions.xml +++ b/docs/reference/user/connection-transitions.xml @@ -1 +1 @@ -7Vxbc6M6Ev41qZp9cAqMAfsxzuXsPpyqqc3DmX0kINvUEPBgnGT2158WdAOSwOaiOCS7dvmCJITU+vqqhivr9vntj9Tb7/5MAhZdzY3g7cq6u5rPV/YCvnnB76LAceyiYJuGQVFkVgWP4X8ZFhpYegwDdhAaZkkSZeFeLPSTOGZ+JpRtkki8xN7bUvdVwaPvRWrpX2GQ7YrS5dypyv/Jwu2OLmM6q6LmkP2mPgK28Y5RNsuLoI5XP3vUVz4r6x4IliYJdMP/Pb/dsogTjQhSTP2hpbYcZMpiHMjpE5DgL150xDHe+D7bZyxQBg8nAmHhYP26CzP2uPd8XvMKSwtlmyTOcIVMXNb6KHBgLyzNGC5/XoSj+oMlzyxLf0MTrLWWuMiIDRtH+lqjPbbY1chOZR4iYFt2XBEB/iAdmmmCK1qjyb+2cZKOIgku9iiS2IZIEnOl0sRpoAmVjaGJq9DkkU/kYzFiShiZLy4HkqVCkHWU+D8nxzeXpAkKvBpNvrM4COPtB9PEkRjnkjQhXVWXsAo5vDRNXjtPnAWCplKnXZuWSVOvz6ssTFnkZeGLqOGaZovX+J6EnOlL/qPJEV2XOGjq4pAcU5/hWXUtJHe0OtNR5qVblikd5cQvJ95tPQg9/dfDWgfeYQccDkdw4B/Tl/IgTY5xkB8ZcHTI0uRnaSbw+k0YRbdJlKT5BawHg7+nv8KuLS8Mck7fFXbcMx1pXGFLWeErd12aNW5eICy3sJKi1qa1rK/dgwGv5lWGc2stb6klewuzH9iG//8P4gTWPf3NK8oDXpO3ioObAoR3yZ7FRckDgAjrW6FTLIigtAvSCvbedESIjAtjIMBkvUe20Rl8AZE9Pl9qtucNDu3jnS9Q8ZMNhi5L67DI+KL2aMdW8C5GMBjsKChFcQanGQAR/sM/AP/cWuPQNwCcc6MnDzTj1Li2a9iujgjdgrRU2IizRgsbfRVwwxxFcNMIe4Nb0o8mGTC60S0pdNM9je7SQhKZThu4Ve+0O265vq6waS6cGjoLCduIaWz1naUhjJRxsLbr6Toecax1PKKo+Hp4XGBUoeyIVkozHq2FhMdz0lZqTxPUBkg1NADnQKH15nG5VTMzChlMLiE/KEVwL8nbKl0r/BZVdetBskPuFihqZStUtGRqNffrVuGsxTJBzhgivHGpjWtjJb7xMpdipoVhXjvuqnyRy0JQHWop835d03bnS9ee25a7REVHkLacxmrNnKdwONplXSUCxaYaOa86m4aTbDYHAMJIw18NUiEaNXJbqToqC6gT4jE2MmVzZWEvr8HznrvmfOEunNKR6oi8zgA/fRnTtRqrNQPckVXLGVPHlhwVU2SI8apFDSgOtHUugFxUg5NBri15WcMNG3mV38nQdmhDoOSt0+hTIphocOlCH0lvveirDBMVf13Rh1Cbsln9PyU3F/L+gohEtb3MmiLSxyNXjecOQ64QmxOR2xD1mH7ottz2I8KvkDBjI2tKR7qcPTnWjNdpHZccOhTbj0cWTvNyMhHqBirkyUUa5J0hORzbFXy21JElx4c1gU++Du1PtIo1ZcdKs1hTNzH0Rw4E9HX23Rs08tQ8GdB+gspFjqG1cvQoZNklINWmHZtWvyiYYsSK7cdjU91z6OFnn9a5n2DTW6JuaeD0hQ/k85zuSFdQX4IPXUcbHNSg6E0UJa887ml8i71nFvyDJ+FFYZ4xZGSpFx/CLEziawU1kNABy7X2onALkcU7H87gIfg1z/UIIRfvBiuewyDg57TjpUfGjGRILygycCY7hJqNyQ5pyKuCIPKPXmHi0wylhHZvbykYLG1WF+Wfjv9s2dEZyn9KR7r4T/Ld6Tqt45L4VWo/nl/VSFPFrw1cWiTQAo68MM7b5EUpAw6DDC6OUS87wvyNH5NgZ9gEEtl5hctaA2i5K6+dn9UUuK+T7eWKcrIMuvVnvDMd6csFIlZ6D1Ollt7w+YyWobtTitHSMY9rrNCk63QVmlL70ULTakgbPGYJJO+HPkcTAwjxxBpBbEqCUqhTYfghto8lxqEXdAdGHZhEWt3Ckq5do+mfXnz0eJvcLeWzw2X+BrvnQU7hZFPd0VGQMmW/juyQgZE5BYra4n6tbaFKuIA1aalBg//ntp6QiaKh4BgD4wBzuo+praMWmfhO2+Bk/7SIKoWz/s38cI9W39SZyxGzL+0lHl+CuVQ3F1y18gYHnuFDtwn1TfKZZLrOWT7FNZiR7Xqx2LK8HyHfptGZb+UdErmjd8pio+u0bmzIm71i+4vk0lhqYOLWi31oNLk7nEpn7gJ381gob+qTBz55xMMkzXbJNom96L4qlfTnLnsm3msO0Z9ITsXNo9McLYia9rsXhiZZEzLOyod+bN7bQZASzkBmajX4Kc9f2P1zIq7ygvAF/m75Xyo67D2uLCtYOL+O/BbetV9QHswwI90+fctvY4WBcm0BP/SPx0fz3owIYiwzQi0/y7zm8VXqTr4u563G6/KK2SHnuryX+f5N7eWeugFiFD2JvUNxMTGluIEE44byYYR010KS7pmpf+w6F2WlyTFk7S4/AZ4gfWpET+DUKUNvmlE75nDstdmAoNtsinh2VTQUm+Xd3/n9NKUW5FbeuSUQxtxsQNf0QcpgKN5T3iAncx/zOvKeWLT2/J/bXN/URP4mf4maw4+8wwH8gfPmIKk89ApweOWDBXoobp6yLW3F0n0DI03CmZR7LcWk9FhE6uMczqv+E350kx3QZgU0pG52WEvBox+aWTIRdS/vtdOmXde9ee3mQePGnSgcPim/Iwh08Ls5X4mcOUNhMpbfKQWh9EGllDQtHE9uWy+O72feKzmxAs92WL8+t1UKwmhoqs9U5IEU77dcvcl/cLvgF+VvQvV4/p4Bg7u2qNBntIk+lMP1cvAQnd3TXW8MyJVc2MgsZ/mOmExJ8RzBZl1DHLTBp5lmnXMQ2yMWgvzqFtfUQfi86BKEb3huxSA/p/RsK8elHqZ+XyerOPcze0k9xaOIqYJ7TorMBZnZpcjEfkYaReLWSNmrVoFqE8+8nxNUT7QYbQQ1uEA6REK+gJcQCQ23INSe5fF5DJCB3HTmsV4mRBQseLIQvrAD8vjoGh/sVsBh9TjHonn1MEzr/m8= \ No newline at end of file +file has moved [here](../legacy/reference/user/connection-transitions.xml) diff --git a/docs/reference/user/connection.md b/docs/reference/user/connection.md index 910c7452881..08c35e1049d 100644 --- a/docs/reference/user/connection.md +++ b/docs/reference/user/connection.md @@ -1,107 +1 @@ -# Connection {#RefConnection} - -Two users can be _connected_ or not. If the users are connected, each of them can: - -* Add the other user to a conversation (which is also a requirement for having a 1-1 conversation or doing a call). - -* See the locale of the other user. - -By default users with personal accounts are not connected. A user can send another user a _connection request_, which can be ignored or accepted by the other user. A user can also block an existing connection. - -Members of the same team are always considered connected, see [Connections between team members](#RefConnectionTeam). - -Internally, connection status is a _directed_ edge from one user to another that is attributed with a relation state and some meta information. If a user has a connection to another user, it can be in one of the six [connection states](#RefConnectionStates). - -## Connection states {#RefConnectionStates} - -### Sent {#RefConnectionSent} - -In order for two users to become connected, one of them performs a _connection request_ and the other one accepts it. Initiating a new connection results in a pending 1-1 conversation to be created with the sender as the sole member. When the connection is accepted, the other user joins the conversation. - -The creator of a new connection (i.e. the sender of the connection request) ends up in this state. From the point of view of the creator, it indicates that a connection request has been sent but not accepted (it might be blocked or ignored). - -### Pending {#RefConnectionPending} - -The recipient of a connection request automatically ends up in this state. -From his point of view, the state indicates that the connection is pending -and awaiting further action (i.e. through accepting, ignoring or blocking it). - -### Blocked {#RefConnectionBlocked} - -When a connection is in this state it indicates that the user does not want to be bothered by the other user, e.g. by receiving messages, calls or being added to conversations. - -Blocking a user does not prevent receiving further messages of that user in existing group conversations where the blocked user is a member. - -When user A blocks user B, the connection restrictions apply to both users -- e.g. A can not add B to conversations, even though it's A who blocked B and not vice-versa. - -### Ignored {#RefConnectionIgnored} - -The recipient of a connection request may decide to explicitly "ignore" the request In this state the sender can continue to send further connection attempts. The recipient can change their mind and accept the request later. - -### Cancelled {#RefConnectionCancelled} - -This is a state that the sender can change to if the connection has not yet been accepted. The state will also change for the recipient, unless blocked. - -### Accepted {#RefConnectionAccepted} - -A connection in this state is fully accepted by a user. The user thus allows the user at the other end of the connection to add him to conversations. - -For two users to be considered "connected", both A->B and B->A connections have to be in the "Accepted" state. - -## Transitions between connection states {#RefConnectionTransitions} - -![Connection state transitions](connection-transitions.png) - -(To edit this diagram, open [connection-transitions.xml](connection-transitions.xml) with .) - -## Connections between team members {#RefConnectionTeam} - -Users belonging to the same team are always implicitly treated as connected, to make it easier for team members to see each other's profiles, create conversations, etc. - -Since there is no explicit connection state between two team members, changing the connection status (e.g. blocking a fellow team member) is impossible. - -# Connection backend internals - -In the regular case of a single backend (no federation involved), and in the easiest case of two users Alice and Adham which want to start talking, the simplified internals involving the services brig and galley and cassandra can be seen as follows: (as of 2021-08) - -![Connection backend internal flow](connections-flow-1-backend.png) - -
-(To edit this diagram, copy the code in this details block to https://swimlanes.io ) - -``` -title: Connections: (no federation) - -note: this is a simplified view of what happens internall inside the backend in the simple case for connection requests. For the full details refer to the code. - -note: Alice sends a connection request to Adham (all on backend A) - -order: Alice, Adham, brig, galley, cassandra - -Alice -> brig: POST /connections - -brig -> cassandra: write in 'connections': Alice-Adham-sent -brig -> cassandra: write in 'connections': Adham-Alice-pending -note brig, galley: when a connection request is sent, that also creates a conversation of type 'connection' containing only the sender: -brig -> galley: /i/conversations/connect -galley -> cassandra: write in conversations: ID-A-A: connection/[Alice] -brig -> Adham: Event: new connection request from Alice - -...: {fas-spinner} - -note Alice, cassandra: Adham reacts and sends a request to accept the connection request - -Adham -> brig: *PUT /connections/* -brig -> cassandra: read 'connections' for Alice-Adham -brig -> cassandra: read 'connections' for Adham-Alice -brig -> cassandra: write in 'connections': Alice-Adham-accept -brig -> cassandra: write in 'connections': Adham-Alice-accept - -note brig, galley: Accepting a connection also leads to the upgrade of the 'connect' conversation to a 'one2one' conversation and adds Adham to the member list -brig -> galley: /i/conversations/:convId/accept/v2 -galley -> cassandra: write in conversations: ID-A-A: one2one/[Alice,Adham] -brig -> Alice: Event: connection request accepted -``` -
- -The connection / one2one conversation ID is deterministically determined using a combination of the two involved user's UUIDs, using the [addv4](https://github.com/wireapp/wire-server/blob/3b1d0c5acee58bb65d8d72e71baf68dd4c0096ae/libs/types-common/src/Data/UUID/Tagged.hs#L67-L83) function. +file has moved [here](../legacy/reference/user/connection.md) diff --git a/docs/reference/user/connections-flow-1-backend.png b/docs/reference/user/connections-flow-1-backend.png index 7231652c45f..e2166d82f99 100644 Binary files a/docs/reference/user/connections-flow-1-backend.png and b/docs/reference/user/connections-flow-1-backend.png differ diff --git a/docs/reference/user/registration.md b/docs/reference/user/registration.md index fee8c310227..fc24c8699ab 100644 --- a/docs/reference/user/registration.md +++ b/docs/reference/user/registration.md @@ -1,200 +1 @@ -# Registration {#RefRegistration} - -_Authors: Artyom Kazak, Matthias Fischmann_ - ---- - -This page describes the "normal" user registration flow. Autoprovisioning is covered separately. - -## Summary {#RefRegistrationSummary} - -The vast majority of our API is only available to Wire users. Unless a user is autoprovisioned, they have to register an account by calling the `POST /register` endpoint. - -Most users also go through [activation](activation.md) -- sharing and verifying an email address and/or phone number with Wire. This can happen either before or after registration. [Certain functionality](activation.md#RefActivationBenefits) is only available to activated users. - -## Standard registration flow {#RefRegistrationStandard} - -During the standard registration flow, the user first calls [`POST /activate/send`](activation.md#RefActivationRequest) to pre-verify their email address or phone number. Phone numbers must be in [E.164][] format. - -[E.164]: https://en.wikipedia.org/wiki/E.164 - -After receiving a six-digit activation code via email/text message, it can be submitted with the registration request via `POST /register`. If the code is correct, the account will be activated immediately. Here is a sample request and response: - -``` -POST /register - -{ - // The name is mandatory - "name": "Pink", - - // 'email', 'phone', or both have to be provided - "email": "pink@example.com", - - // The password is optional - "password": "secret", - - // 6-digit 'email_code' or 'phone_code' - "email_code": "123456" -} -``` - -``` -201 Created -Set-Cookie: zuid=... - -{ - "accent_id": 0, - "assets": [], - "email": "pink@example.com", - "id": "4e1b823a-7961-4e70-bff5-30c08555c89f", - "locale": "en", - "managed_by": "wire", - "name": "Pink", - "picture": [] -} -``` - -The response contains an access cookie that can be used to access the rest of the backend API. The cookie can be assigned a label by adding a `"label": <...>` field when calling `/register`. - -If the code is incorrect or if an incorrect code has been tried enough times, the response will be as follows: - -``` -404 Not Found - -{ - "code": 404, - "label": "invalid-code", - "message": "Invalid activation code" -} -``` - -## Registration without pre-verification {#RefRegistrationNoPreverification} - -_NOTE: This flow is currently not used by any clients. At least this was the state on 2020-05-28_ - -It is also possible to call `POST /register` without verifying the email address or phone number, in which case the account will have to be activated later by calling [`POST /activate`](activation.md#RefActivationSubmit). Sample API request and response: - -``` -POST /register - -{ - // The name is mandatory - "name": "Pink", - - // 'email', 'phone', or both have to be provided - "email": "pink@example.com", - - // The password is optional - "password": "secret" -} -``` - -``` -201 Created -Set-Cookie: zuid=... - -{ - "accent_id": 0, - "assets": [], - "email": "pink@example.com", - "id": "c193136a-55fb-4534-ad72-d02a72bb16af", - "locale": "en", - "managed_by": "wire", - "name": "Pink", - "picture": [] -} -``` - -A verification email will be sent to the email address (if provided), and a verification text message will be sent to the phone number (also, if provided). - -## Anonymous registration, aka "Wireless" {#RefRegistrationWireless} - -A user can be created without either email or phone number, in which case only `"name"` is required. The `"name"` does not have to be unique. This feature is used for [guest rooms](https://wire.com/en/features/encrypted-guest-rooms/). - -An anonymous, non-activated account is only usable for a period of time specified in `brig.yaml` at `zauth.authSettings.sessionTokenTimeout`, which is set to 1 day for Wire production. (The access cookie returned by `/register` can not be refreshed, and an anonymous user can not use `/login` to get a new cookie.) - -Sample API request and response: - -``` -POST /register - -{ - "name": "Pink" -} -``` - -``` -201 Created -Set-Cookie: zuid=... - -{ - "accent_id": 0, - "assets": [], - "expires_at": "2019-04-18T14:09:43.732Z", - "id": "1914b4e6-eb72-4943-8925-06314b24ed68", - "locale": "en", - "managed_by": "wire", - "name": "Pink" - "picture": [], -} -``` - -## Blocking creation of personal users, new teams {#RefRestrictRegistration} - -[moved here](https://docs.wire.com/how-to/install/configuration-options.html#blocking-creation-of-personal-users-new-teams) - -### Details - -You can find the exhaustive list of all routes here: - -https://github.com/wireapp/wire-server-deploy/blob/de7e3e8c709f8baaae66b1540a1778871044f170/charts/nginz/values.yaml#L35-L371 - -The paths not cryptographically authenticated can be found by searching for the `disable_zauth:` flag (must be true for `env: prod` or `env: all`). - -Two of them allow users to create new users or teams: - -- `/register` -- `/activate` - -These end-points support 5 flows: - -1. new team account -2. new personal (teamless) account -3. invitation code from team, new member -4. ephemeral user -5. [not supported by clients] new *inactive* user account - -We need an option to block 1, 2, 5 on-prem; 3, 4 should remain available (no block option). There are also provisioning flows via SAML or SCIM, which are not critical. In short, this could refactored into: - - * Allow team members to register (via email/phone or SSO) - * Allow ephemeral users - -During registration, we can take advantage of [NewUserOrigin](https://github.com/wireapp/wire-server/blob/a89b9cd818997e7837e5d0938ecfd90cf8dd9e52/libs/wire-api/src/Wire/API/User.hs#L625); we're particularly interested in `NewUserOriginTeamUser` --> only `NewTeamMember` or `NewTeamMemberSSO` should be accepted. In case this is a `Nothing`, we need to check if the user expires, i.e., if the user has no identity (and thus `Ephemeral`). - -So `/register` should only succeed iff at least one of these conditions is true: - -``` -import Brig.Types.User -isNewUserTeamMember || isNewUserEphemeral -``` - -The rest of the unauthorized end-points is safe: - -- `/password-reset` -- `/delete`: similar to password reset, for deleting a personal account with password. -- `/login` -- `/login/send` -- `/access` -- `/sso/initiate-login`: authenticated via IdP. -- `/sso/finalize-login`: authenticated via IdP. -- `/sso`: authenticated via IdP or ok to expose to world (`/metadata`) -- `/scim/v2`: authenticated via HTTP simple auth. -- `~* ^/teams/invitations/info$`: only `GET`; requires invitation code. -- `~* ^/teams/invitations/by-email$`: only `HEAD`. -- `/invitations/info`: discontinued feature, can be removed from nginz config. -- `/conversations/code-check`: link validatoin for ephemeral/guest users. -- `/provider/*`: bots need to be registered to a team before becoming active. so if an attacker does not get access to a team, they cannot deploy a bot. -- `~* ^/custom-backend/by-domain/([^/]*)$`: only `GET`; only exposes a list of domains that has is maintained through an internal end-point. used to redirect stock clients from the cloud instance to on-prem instances. -- `~* ^/teams/api-docs`: only `GET`; swagger for part of the rest API. safe: it is trivial to identify the software that is running on the instance, and from there it is trivial to get to the source on github, where this can be obtained easily, and more. -- `/billing`: separate billing service, usually not installed for on-prem instances. -- `/calling-test`: separate testing service that has its own authentication. +file has moved [here](../legacy/reference/user/registration.md) diff --git a/docs/reference/user/rich-info.md b/docs/reference/user/rich-info.md index e6ecb60c297..c195e062fe0 100644 --- a/docs/reference/user/rich-info.md +++ b/docs/reference/user/rich-info.md @@ -1,148 +1 @@ -# Rich info {#RefRichInfo} - -_Author: Artyom Kazak_ - ---- - -This page describes a part of the user profile called "Rich info". The corresponding feature is called "Rich profiles". - -## Summary {#RefRichInfoSummary} - -For every team user we can store a list of key-value pairs that are displayed in the user profile. This is similar to "custom profile fields" in Slack and other enterprise messengers. - -Different users can have different sets of fields; there is no team-wide schema for fields. All field values are strings. Fields are passed as an ordered list, and the order information is preserved when displaying fields in client apps. - -Only team members and partners can see the user's rich info. - -## API {#RefRichInfoApi} - -### Querying rich info {#RefRichInfoGet} - -`GET /users/:uid/rich-info`. Sample output: - -```json -{ - "version": 0, - "fields": [ - { - "type": "Department", - "value": "Sales & Marketing" - }, - { - "type": "Favorite color", - "value": "Blue" - } - ] -} -``` - -If the requesting user is not allowed to see rich info, error code 403 is returned with the `"insufficient-permissions"` error label. - -Otherwise, if the rich info is missing, an empty field list is returned: - -```json -{ - "version": 0, - "fields": [] -} -``` - -### Setting rich info {#RefRichInfoPut} - -**Not implemented yet.** Currently the only way to set rich info is via SCIM. - -### Events {#RefRichInfoEvents} - -**Not implemented yet.** - -When user's rich info changes, the backend sends out an event to all team members: - -```json -{ - "type": "user.rich-info-update", - "user": { - "id": "" - } -} -``` - -Connected users who are not members of user's team will not receive an event (nor can they query user's rich info by other means). - -## SCIM support {#RefRichInfoScim} - -Rich info can be pushed to Wire by setting JSON keys under the `"urn:ietf:params:scim:schemas:extension:wire:1.0:User"` extension. Both `PUT /scim/v2/Users/:id` , `PATCH /scim/v2/Users/:id` and `POST /scim/v2/Users/:id` can contain rich info. Here is an example for `PUT`: - -```javascript -PUT /scim/v2/Users/:id - -{ - ..., - "urn:ietf:params:scim:schemas:extension:wire:1.0:User": { - "Department": "Sales & Marketing", - "FavoriteColor": "Blue" - } -} -``` - -Here is an example for `PATCH`: - -```json -PATCH /scim/v2/Users/:id - -{ - "schemas": [ - "urn:ietf:params:scim:api:messages:2.0:PatchOp" - ], - "operations": [ - { - "op": "add", - "path": "urn:ietf:params:scim:schemas:extension:wire:1.0:User:Department", - "value": "Development " - }, - { - "op": "replace", - "path": "urn:ietf:params:scim:schemas:extension:wire:1.0:User:Country", - "value": "Germany" - }, - { - "op": "remove", - "path": "urn:ietf:params:scim:schemas:extension:wire:1.0:User:City" - } - ] -} - -``` - -Rich info set via SCIM can be queried by doing a `GET /scim/v2/Users` or `GET /scim/v2/Users/:id` query. - -### Set up SCIM RichInfo mapping in Azure {#RefRichInfoScimAgents} - -Go to your provisioning page - -![image](https://user-images.githubusercontent.com/628387/119977043-393b3000-bfb8-11eb-9e5b-18a955ca3181.png) - -Click "Edit attribute mappings" - -Then click "Mappings" And then click **Synchronize Azure Active Directory Users to _appname_** -![image](https://user-images.githubusercontent.com/628387/119977488-c9797500-bfb8-11eb-81b8-46376f5fdadb.png) - -Click "Show Advanced options" and then **Edit attribute list for _appname_** -![image](https://user-images.githubusercontent.com/628387/119977905-3f7ddc00-bfb9-11eb-90e2-28da82c6f13e.png) - -Add a new attribute name. The type should be `String` and the name should be prefixed with `urn:ietf:params:scim:schemas:extension:wire:1.0:User:` -e.g. `urn:ietf:params:scim:schemas:extension:wire:1.0:User:Location` - -![image](https://user-images.githubusercontent.com/628387/119978050-70f6a780-bfb9-11eb-8919-93e32bf76d79.png) - -Hit **Save** and afterwards hit **Add New Mapping** - -Select the Azure AD Source attribute you want to map, and map it to the custom **Target Attribute** that you just added. -![image](https://user-images.githubusercontent.com/628387/119978316-c5018c00-bfb9-11eb-9290-2076ac1a05df.png) - - - -## Limitations {#RefRichInfoLimitations} - -* The whole of user-submitted information (field names and values) cannot exceed 5000 characters in length. There are no limitations on the number of fields, or the maximum of individual field names or values. - -* Field values can not be empty (`""`). If they are empty, the corresponding field will be removed. +file has moved [here](../legacy/reference/user/rich-info.md) diff --git a/tools/db/move-team/README.md b/tools/db/move-team/README.md index dd40c4ad0c4..aa6e4380fde 100644 --- a/tools/db/move-team/README.md +++ b/tools/db/move-team/README.md @@ -10,7 +10,7 @@ with a a single team id. This can be useful for testing. `src/Schema.hs` is created by the executable `move-team-generate`. You can also use `ParseSchema.debugwrite` to recreate it from a ghci. -It parses the cql `docs/reference/cassandra-schema.cql` +It parses the cql `/cassandra-schema.cql` Note: `move-team` has not been thoroughly tested yet. diff --git a/tools/db/move-team/src/ParseSchema.hs b/tools/db/move-team/src/ParseSchema.hs index ac3952c6655..369bd985e29 100644 --- a/tools/db/move-team/src/ParseSchema.hs +++ b/tools/db/move-team/src/ParseSchema.hs @@ -483,12 +483,12 @@ projectFile relativeFilename = debug :: IO () debug = do - cassandraSchema <- projectFile "docs/reference/cassandra-schema.cql" + cassandraSchema <- projectFile "cassandra-schema.cql" withArgs [cassandraSchema] main debugwrite :: IO () debugwrite = do - cassandraSchema <- projectFile "docs/reference/cassandra-schema.cql" + cassandraSchema <- projectFile "cassandra-schema.cql" outputFile <- projectFile "tools/db/move-team/src/Schema.hs" withArgs [cassandraSchema, "--output=" <> outputFile] main