diff options
author | dcorbacho <dparracorbacho@piotal.io> | 2020-11-18 14:27:41 +0000 |
---|---|---|
committer | dcorbacho <dparracorbacho@piotal.io> | 2020-11-18 14:27:41 +0000 |
commit | f23a51261d9502ec39df0f8db47ba6b22aa7659f (patch) | |
tree | 53dcdf46e7dc2c14e81ee960bce8793879b488d3 /deps/rabbitmq_management/src | |
parent | afa2c2bf6c7e0e9b63f4fb53dc931c70388e1c82 (diff) | |
parent | 9f6d64ec4a4b1eeac24d7846c5c64fd96798d892 (diff) | |
download | rabbitmq-server-git-stream-timestamp-offset.tar.gz |
Merge remote-tracking branch 'origin/master' into stream-timestamp-offsetstream-timestamp-offset
Diffstat (limited to 'deps/rabbitmq_management/src')
88 files changed, 9044 insertions, 0 deletions
diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_app.erl b/deps/rabbitmq_management/src/rabbit_mgmt_app.erl new file mode 100644 index 0000000000..256ae56cc6 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_app.erl @@ -0,0 +1,206 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_app). + +-behaviour(application). +-export([start/2, stop/1, reset_dispatcher/1]). + +-ifdef(TEST). +-export([get_listeners_config/0]). +-endif. + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). + +-define(TCP_CONTEXT, rabbitmq_management_tcp). +-define(TLS_CONTEXT, rabbitmq_management_tls). +-define(DEFAULT_PORT, 15672). +-define(DEFAULT_TLS_PORT, 15671). + +-rabbit_boot_step({rabbit_management_load_definitions, + [{description, "Imports definition file at management.load_definitions"}, + {mfa, {rabbit_mgmt_load_definitions, boot, []}}, + {enables, empty_db_check}]}). + +start(_Type, _StartArgs) -> + case application:get_env(rabbitmq_management_agent, disable_metrics_collector, false) of + false -> + start(); + true -> + rabbit_log:warning("Metrics collection disabled in management agent, " + "management only interface started", []), + start() + end. + +stop(_State) -> + unregister_all_contexts(), + ok. + +%% At the point at which this is invoked we have both newly enabled +%% apps and about-to-disable apps running (so that +%% rabbit_mgmt_reset_handler can look at all of them to find +%% extensions). Therefore we have to explicitly exclude +%% about-to-disable apps from our new dispatcher. +reset_dispatcher(IgnoreApps) -> + unregister_all_contexts(), + start_configured_listeners(IgnoreApps, false). + +-spec start_configured_listeners([atom()], boolean()) -> ok. +start_configured_listeners(IgnoreApps, NeedLogStartup) -> + [start_listener(Listener, IgnoreApps, NeedLogStartup) + || Listener <- get_listeners_config()], + ok. + +get_listeners_config() -> + Listeners = case {has_configured_legacy_listener(), + has_configured_tcp_listener(), + has_configured_tls_listener()} of + {false, false, false} -> + %% nothing is configured + [get_tcp_listener()]; + {false, false, true} -> + [get_tls_listener()]; + {false, true, false} -> + [get_tcp_listener()]; + {false, true, true} -> + [get_tcp_listener(), + get_tls_listener()]; + {true, false, false} -> + [get_legacy_listener()]; + {true, false, true} -> + [get_legacy_listener(), + get_tls_listener()]; + {true, true, false} -> + %% This combination makes some sense: + %% legacy listener can be used to set up TLS :/ + [get_legacy_listener(), + get_tcp_listener()]; + {true, true, true} -> + %% what is happening? + rabbit_log:warning("Management plugin: TCP, TLS and a legacy (management.listener.*) listener are all configured. " + "Only two listeners at a time are supported. " + "Ignoring the legacy listener"), + [get_tcp_listener(), + get_tls_listener()] + end, + maybe_disable_sendfile(Listeners). + +maybe_disable_sendfile(Listeners) -> + DisableSendfile = #{sendfile => false}, + F = fun(L0) -> + CowboyOptsL0 = proplists:get_value(cowboy_opts, L0, []), + CowboyOptsM0 = maps:from_list(CowboyOptsL0), + CowboyOptsM1 = maps:merge(DisableSendfile, CowboyOptsM0), + CowboyOptsL1 = maps:to_list(CowboyOptsM1), + L1 = lists:keydelete(cowboy_opts, 1, L0), + [{cowboy_opts, CowboyOptsL1}|L1] + end, + lists:map(F, Listeners). + +has_configured_legacy_listener() -> + has_configured_listener(listener). + +has_configured_tcp_listener() -> + has_configured_listener(tcp_config). + +has_configured_tls_listener() -> + has_configured_listener(ssl_config). + +has_configured_listener(Key) -> + case application:get_env(rabbitmq_management, Key, undefined) of + undefined -> false; + _ -> true + end. + +get_legacy_listener() -> + {ok, Listener0} = application:get_env(rabbitmq_management, listener), + {ok, Listener1} = ensure_port(tcp, Listener0), + Listener1. + +get_tls_listener() -> + {ok, Listener0} = application:get_env(rabbitmq_management, ssl_config), + {ok, Listener1} = ensure_port(tls, Listener0), + Port = proplists:get_value(port, Listener1), + case proplists:get_value(cowboy_opts, Listener0) of + undefined -> + [ + {port, Port}, + {ssl, true}, + {ssl_opts, Listener0} + ]; + CowboyOpts -> + Listener1 = lists:keydelete(cowboy_opts, 1, Listener0), + [ + {port, Port}, + {ssl, true}, + {ssl_opts, Listener1}, + {cowboy_opts, CowboyOpts} + ] + end. + +get_tcp_listener() -> + Listener0 = application:get_env(rabbitmq_management, tcp_config, []), + {ok, Listener1} = ensure_port(tcp, Listener0), + Listener1. + +start_listener(Listener, IgnoreApps, NeedLogStartup) -> + {Type, ContextName} = case is_tls(Listener) of + true -> {tls, ?TLS_CONTEXT}; + false -> {tcp, ?TCP_CONTEXT} + end, + {ok, _} = register_context(ContextName, Listener, IgnoreApps), + case NeedLogStartup of + true -> log_startup(Type, Listener); + false -> ok + end, + ok. + +register_context(ContextName, Listener, IgnoreApps) -> + Dispatcher = rabbit_mgmt_dispatcher:build_dispatcher(IgnoreApps), + rabbit_web_dispatch:register_context_handler( + ContextName, Listener, "", + Dispatcher, "RabbitMQ Management"). + +unregister_all_contexts() -> + rabbit_web_dispatch:unregister_context(?TCP_CONTEXT), + rabbit_web_dispatch:unregister_context(?TLS_CONTEXT). + +ensure_port(tls, Listener) -> + do_ensure_port(?DEFAULT_TLS_PORT, Listener); +ensure_port(tcp, Listener) -> + do_ensure_port(?DEFAULT_PORT, Listener). + +do_ensure_port(Port, Listener) -> + %% include default port if it's not provided in the config + %% as Cowboy won't start if the port is missing + M0 = maps:from_list(Listener), + M1 = maps:merge(#{port => Port}, M0), + {ok, maps:to_list(M1)}. + +log_startup(tcp, Listener) -> + rabbit_log:info("Management plugin: HTTP (non-TLS) listener started on port ~w", [port(Listener)]); +log_startup(tls, Listener) -> + rabbit_log:info("Management plugin: HTTPS listener started on port ~w", [port(Listener)]). + + +port(Listener) -> + proplists:get_value(port, Listener, ?DEFAULT_PORT). + +is_tls(Listener) -> + case proplists:get_value(ssl, Listener) of + undefined -> false; + false -> false; + _ -> true + end. + +start() -> + %% Modern TCP listener uses management.tcp.*. + %% Legacy TCP (or TLS) listener uses management.listener.*. + %% Modern TLS listener uses management.ssl.* + start_configured_listeners([], true), + rabbit_mgmt_sup_sup:start_link(). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_cors.erl b/deps/rabbitmq_management/src/rabbit_mgmt_cors.erl new file mode 100644 index 0000000000..3871c1f8e2 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_cors.erl @@ -0,0 +1,84 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +%% Useful documentation about CORS: +%% * https://tools.ietf.org/html/rfc6454 +%% * https://www.w3.org/TR/cors/ +%% * https://staticapps.org/articles/cross-domain-requests-with-cors/ +-module(rabbit_mgmt_cors). + +-export([set_headers/2]). + +set_headers(ReqData, Module) -> + %% Send vary: origin by default if nothing else was set. + ReqData1 = cowboy_req:set_resp_header(<<"vary">>, <<"origin">>, ReqData), + case match_origin(ReqData1) of + false -> + ReqData1; + Origin -> + ReqData2 = case cowboy_req:method(ReqData1) of + <<"OPTIONS">> -> handle_options(ReqData1, Module); + _ -> ReqData1 + end, + ReqData3 = cowboy_req:set_resp_header(<<"access-control-allow-origin">>, + Origin, + ReqData2), + cowboy_req:set_resp_header(<<"access-control-allow-credentials">>, + "true", + ReqData3) + end. + +%% Set max-age from configuration (default: 30 minutes). +%% Set allow-methods from what is defined in Module:allowed_methods/2. +%% Set allow-headers to the same as the request (accept all headers). +handle_options(ReqData0, Module) -> + MaxAge = application:get_env(rabbitmq_management, cors_max_age, 1800), + Methods = case erlang:function_exported(Module, allowed_methods, 2) of + false -> [<<"HEAD">>, <<"GET">>, <<"OPTIONS">>]; + true -> element(1, Module:allowed_methods(undefined, undefined)) + end, + AllowMethods = string:join([binary_to_list(M) || M <- Methods], ", "), + ReqHeaders = cowboy_req:header(<<"access-control-request-headers">>, ReqData0), + + ReqData1 = case MaxAge of + undefined -> ReqData0; + _ -> cowboy_req:set_resp_header(<<"access-control-max-age">>, + integer_to_list(MaxAge), + ReqData0) + end, + ReqData2 = case ReqHeaders of + undefined -> ReqData1; + _ -> cowboy_req:set_resp_header(<<"access-control-allow-headers">>, + ReqHeaders, + ReqData0) + end, + cowboy_req:set_resp_header(<<"access-control-allow-methods">>, + AllowMethods, + ReqData2). + +%% If the origin header is missing or "null", we disable CORS. +%% Otherwise, we only enable it if the origin is found in the +%% cors_allow_origins configuration variable, or if "*" is (it +%% allows all origins). +match_origin(ReqData) -> + case cowboy_req:header(<<"origin">>, ReqData) of + undefined -> false; + <<"null">> -> false; + Origin -> + AllowedOrigins = application:get_env(rabbitmq_management, + cors_allow_origins, []), + case lists:member(binary_to_list(Origin), AllowedOrigins) of + true -> + Origin; + false -> + %% Maybe the configuration explicitly allows "*". + case lists:member("*", AllowedOrigins) of + true -> Origin; + false -> false + end + end + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_csp.erl b/deps/rabbitmq_management/src/rabbit_mgmt_csp.erl new file mode 100644 index 0000000000..80bba64fc2 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_csp.erl @@ -0,0 +1,28 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +%% Sets CSP header(s) on the response if configured, +%% see https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP. + +-module(rabbit_mgmt_csp). + +-export([set_headers/1]). + +-define(CSP_HEADER, <<"content-security-policy">>). + +%% +%% API +%% + +set_headers(ReqData) -> + case application:get_env(rabbitmq_management, content_security_policy) of + undefined -> ReqData; + {ok, Value} -> + cowboy_req:set_resp_header(?CSP_HEADER, + rabbit_data_coercion:to_binary(Value), + ReqData) + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_db.erl b/deps/rabbitmq_management/src/rabbit_mgmt_db.erl new file mode 100644 index 0000000000..c45e7b86f6 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_db.erl @@ -0,0 +1,799 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_db). + +%% pg2 is deprecated in OTP 23. +-compile(nowarn_deprecated_function). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_metrics.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit_core_metrics.hrl"). + +-behaviour(gen_server2). + +-export([start_link/0]). + +-export([augment_exchanges/3, augment_queues/3, + augment_nodes/2, augment_vhosts/2, + get_channel/2, get_connection/2, + get_all_channels/1, get_all_connections/1, + get_all_consumers/0, get_all_consumers/1, + get_overview/2, get_overview/1]). + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3, handle_pre_hibernate/1, + format_message_queue/2]). + +-import(rabbit_misc, [pget/3]). + +-type maybe_slide() :: exometer_slide:slide() | not_found. +-type slide_data() :: #{atom() => {maybe_slide(), maybe_slide()}}. +-type maybe_range() :: rabbit_mgmt_stats:maybe_range(). +-type ranges() :: {maybe_range(), maybe_range(), maybe_range(), maybe_range()}. +-type mfargs() :: {module(), atom(), [any()]}. +-type lookup_key() :: atom() | {atom(), any()}. + +-define(NO_RANGES, {no_range, no_range, no_range, no_range}). + +-define(DEFAULT_TIMEOUT, 30000). + +%% The management database responds to queries from the various +%% rabbit_mgmt_wm_* modules. It calls out to all rabbit nodes to fetch +%% node-local data and aggregates it before returning it. It uses a worker- +%% pool to provide a degree of parallelism. +%% +%% The management database reads metrics and stats written by the +%% rabbit_mgmt_metrics_collector(s). +%% +%% The metrics collectors (there is one for each stats table - see ?TABLES +%% in rabbit_mgmt_metrics.hrl) periodically read their corresponding core +%% metrics ETS tables and aggregate the data into the management specific ETS +%% tables. +%% +%% The metric collectors consume two types of core metrics: created (when an +%% object is created, containing immutable facts about it) and stats (emitted on +%% a timer, with mutable facts about the object). Deleted events are handled +%% by the rabbit_mgmt_metrics_gc process. +%% In this context "objects" means connections, channels, exchanges, queues, +%% consumers, vhosts and nodes. Note that we do not care about users, +%% permissions, bindings, parameters or policies. +%% +%% Connections and channels are identified by pids. Queues and +%% exchanges are identified by names (which are #resource{}s). VHosts +%% and nodes are identified by names which are binaries. And consumers +%% are identified by {ChPid, QName, CTag}. +%% +%% The management collectors records the "created" metrics for +%% connections, channels and consumers, and can thus be authoritative +%% about those objects. For queues, exchanges and nodes we go to +%% Mnesia to find out the immutable details of the objects. +%% +%% For everything other than consumers, the collectors can then augment +%% these immutable details with stats, as the object changes. (We +%% never emit anything very interesting about consumers). +%% +%% Stats on the inbound side are referred to as coarse and +%% fine-grained. Fine grained statistics are the message rates +%% maintained by channels and associated with tuples: {publishing +%% channel, exchange}, {publishing channel, exchange, queue} and +%% {queue, consuming channel}. Coarse grained stats are everything +%% else and are associated with only one object, not a tuple. +%% +%% Within the management database though we rearrange things a bit: we +%% refer to basic stats, simple stats and detail stats. +%% +%% Basic stats are those coarse grained stats for which we do not +%% retain a history and do not perform any calculations - +%% e.g. connection.state or channel.prefetch_count. +%% +%% Simple stats are those for which we do history / calculations which +%% are associated with one object *after aggregation* - so these might +%% originate with coarse grained stats - e.g. connection.send_oct or +%% queue.messages_ready. But they might also originate from fine +%% grained stats which have been aggregated - e.g. the message rates +%% for a vhost or queue. +%% +%% Finally, detailed stats are those for which we do history / +%% calculations which are associated with two objects. These +%% have to have originated as fine grained stats, but can still have +%% been aggregated. +%% +%% Created metrics and basic stats are stored in ETS tables by object. +%% Simple and detailed stats (which only differ depending on how +%% they're keyed) are stored in aggregated stats tables +%% (see rabbit_mgmt_stats.erl and include/rabbit_mgmt_metrics.hrl) +%% +%% Keys from simple and detailed stats are aggregated in several +%% records, stored in different ETS tables. We store a base counter +%% for everything that happened before the samples we have kept, +%% and a series of records which add the timestamp as part of the key. +%% +%% There is also a GC process to handle the deleted/closed +%% rabbit events to remove the corresponding objects from the aggregated +%% stats ETS tables. +%% +%% We also have an old_aggr_stats table to let us calculate instantaneous +%% rates, in order to apportion simple / detailed stats into time +%% slices as they come in. These instantaneous rates are not returned +%% in response to any query, the rates shown in the API are calculated +%% at query time. old_aggr_stats contains both coarse and fine +%% entries. Coarse entries are pruned when the corresponding object is +%% deleted, and fine entries are pruned when the emitting channel is +%% closed, and whenever we receive new fine stats from a channel. So +%% it's quite close to being a cache of "the previous stats we +%% received". +%% +%% Overall the object is to do some aggregation when metrics are read +%% and only aggregate metrics between nodes at query time. + +%%---------------------------------------------------------------------------- +%% API +%%---------------------------------------------------------------------------- + +start_link() -> + gen_server2:start_link({local, ?MODULE}, ?MODULE, [], []). + +augment_exchanges(Xs, Ranges, basic) -> + submit(fun(Interval) -> list_exchange_stats(Ranges, Xs, Interval) end); +augment_exchanges(Xs, Ranges, _) -> + submit(fun(Interval) -> detail_exchange_stats(Ranges, Xs, Interval) end). + +%% we can only cache if no ranges are requested. +%% The mgmt ui doesn't use ranges for queue listings +-spec augment_queues([proplists:proplist()], ranges(), basic | full) -> any(). +augment_queues(Qs, ?NO_RANGES = Ranges, basic) -> + submit_cached(queues, + fun(Interval, Queues) -> + list_queue_stats(Ranges, Queues, Interval) + end, Qs, max(60000, length(Qs) * 2)); +augment_queues(Qs, Ranges, basic) -> + submit(fun(Interval) -> list_queue_stats(Ranges, Qs, Interval) end); +augment_queues(Qs, Ranges, _) -> + submit(fun(Interval) -> detail_queue_stats(Ranges, Qs, Interval) end). + +augment_vhosts(VHosts, Ranges) -> + submit(fun(Interval) -> vhost_stats(Ranges, VHosts, Interval) end). + +augment_nodes(Nodes, Ranges) -> + submit(fun(Interval) -> node_stats(Ranges, Nodes, Interval) end). + +get_channel(Name, Ranges) -> + submit(fun(Interval) -> + case created_stats_delegated(Name, channel_created_stats) of + not_found -> not_found; + Ch -> [Result] = + detail_channel_stats(Ranges, [Ch], Interval), + Result + end + end). + +get_connection(Name, Ranges) -> + submit(fun(Interval) -> + case created_stats_delegated(Name, connection_created_stats) of + not_found -> not_found; + C -> + [Result] = connection_stats(Ranges, [C], Interval), + Result + end + end). + +get_all_channels(?NO_RANGES = Ranges) -> + submit_cached(channels, + fun(Interval) -> + Chans = created_stats_delegated(channel_created_stats), + list_channel_stats(Ranges, Chans, Interval) + end); +get_all_channels(Ranges) -> + submit(fun(Interval) -> + Chans = created_stats_delegated(channel_created_stats), + list_channel_stats(Ranges, Chans, Interval) + end). + +get_all_connections(?NO_RANGES = Ranges) -> + submit_cached(connections, + fun(Interval) -> + Chans = created_stats_delegated(connection_created_stats), + connection_stats(Ranges, Chans, Interval) + end); +get_all_connections(Ranges) -> + submit(fun(Interval) -> + Chans = created_stats_delegated(connection_created_stats), + connection_stats(Ranges, Chans, Interval) + end). + +get_all_consumers() -> get_all_consumers(all). +get_all_consumers(VHosts) -> + submit(fun(_Interval) -> consumers_stats(VHosts) end). + +get_overview(Ranges) -> get_overview(all, Ranges). +get_overview(User, Ranges) -> + submit(fun(Interval) -> overview(User, Ranges, Interval) end). + +%%---------------------------------------------------------------------------- +%% Internal, gen_server2 callbacks +%%---------------------------------------------------------------------------- + +-record(state, {interval}). + +init([]) -> + {ok, Interval} = application:get_env(rabbit, collect_statistics_interval), + rabbit_log:info("Statistics database started."), + {ok, #state{interval = Interval}, hibernate, + {backoff, ?HIBERNATE_AFTER_MIN, ?HIBERNATE_AFTER_MIN, ?DESIRED_HIBERNATE}}. + +handle_call(_Request, _From, State) -> + reply(not_understood, State). + +handle_cast(_Request, State) -> + noreply(State). + +handle_info(_Info, State) -> + noreply(State). + +terminate(_Arg, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +reply(Reply, NewState) -> {reply, Reply, NewState, hibernate}. +noreply(NewState) -> {noreply, NewState, hibernate}. + +handle_pre_hibernate(State) -> + %% rabbit_event can end up holding on to some memory after a busy + %% workout, but it's not a gen_server so we can't make it + %% hibernate. The best we can do is forcibly GC it here (if + %% rabbit_mgmt_db is hibernating the odds are rabbit_event is + %% quiescing in some way too). + _ = rpc:multicall( + rabbit_nodes:all_running(), rabbit_mgmt_db_handler, gc, []), + {hibernate, State}. + +format_message_queue(Opt, MQ) -> rabbit_misc:format_message_queue(Opt, MQ). + +%%---------------------------------------------------------------------------- +%% Internal, utilities +%%---------------------------------------------------------------------------- + +pget(Key, List) -> pget(Key, List, unknown). + +-type id_name() :: 'name' | 'route' | 'pid'. +-spec id_name(atom()) -> id_name(). + +%% id_name() and id() are for use when handling events, id_lookup() +%% for when augmenting. The difference is that when handling events a +%% queue name will be a resource, but when augmenting we will be +%% passed a queue proplist that will already have been formatted - +%% i.e. it will have name and vhost keys. +id_name(node_stats) -> name; +id_name(node_node_stats) -> route; +id_name(vhost_stats) -> name; +id_name(queue_stats) -> name; +id_name(exchange_stats) -> name; +id_name(channel_stats) -> pid; +id_name(connection_stats) -> pid. + +id(Type, List) -> pget(id_name(Type), List). + +id_lookup(queue_stats, List) -> + rabbit_misc:r(pget(vhost, List), queue, pget(name, List)); +id_lookup(exchange_stats, List) -> + rabbit_misc:r(pget(vhost, List), exchange, pget(name, List)); +id_lookup(Type, List) -> + id(Type, List). + + +%%---------------------------------------------------------------------------- +%% Internal, querying side api +%%---------------------------------------------------------------------------- + +overview(User, Ranges, Interval) -> + VHosts = case User of + all -> rabbit_vhost:list_names(); + _ -> rabbit_mgmt_util:list_visible_vhosts_names(User) + end, + DataLookup = get_data_from_nodes({rabbit_mgmt_data, overview_data, + [User, Ranges, VHosts]}), + MessageStats = lists:append( + [format_range(DataLookup, vhost_stats_fine_stats, + pick_range(fine_stats, Ranges), Interval), + format_range(DataLookup, vhost_msg_rates, + pick_range(queue_msg_rates, Ranges), Interval), + format_range(DataLookup, vhost_stats_deliver_stats, + pick_range(deliver_get, Ranges), Interval)]), + + ChurnRates = format_range(DataLookup, connection_churn_rates, + pick_range(queue_msg_counts, Ranges), Interval), + QueueStats = format_range(DataLookup, vhost_msg_stats, + pick_range(queue_msg_counts, Ranges), Interval), + %% Filtering out the user's consumers would be rather expensive so let's + %% just not show it + Consumers = case User of + all -> [{consumers, maps:get(consumers_count, DataLookup)}]; + _ -> [] + end, + ObjectTotals = Consumers ++ + [{queues, length([Q || V <- VHosts, Q <- rabbit_amqqueue:list(V)])}, + {exchanges, length([X || V <- VHosts, X <- rabbit_exchange:list(V)])}, + {connections, maps:get(connections_count, DataLookup)}, + {channels, maps:get(channels_count, DataLookup)}], + + [{message_stats, MessageStats}, + {churn_rates, ChurnRates}, + {queue_totals, QueueStats}, + {object_totals, ObjectTotals}, + {statistics_db_event_queue, event_queue()}]. % TODO: event queue? + +event_queue() -> + lists:foldl(fun ({T, _}, Sum) -> + case whereis(rabbit_mgmt_metrics_collector:name(T)) of + P when is_pid(P) -> + {message_queue_len, Len} = + erlang:process_info(P, message_queue_len), + Sum + Len; + _ -> Sum + end + end, 0, ?CORE_TABLES). + +consumers_stats(VHost) -> + Data = get_data_from_nodes({rabbit_mgmt_data, consumer_data, [VHost]}), + Consumers = rabbit_mgmt_data_compat:fill_consumer_active_fields( + [V || {_,V} <- maps:to_list(Data)]), + ChPids = [ pget(channel_pid, Con) + || Con <- Consumers, [] =:= pget(channel_details, Con)], + ChDets = get_channel_detail_lookup(ChPids), + [merge_channel_into_obj(Con, ChDets) || Con <- Consumers]. + +-spec list_queue_stats(ranges(), [proplists:proplist()], integer()) -> + [proplists:proplist()]. +list_queue_stats(Ranges, Objs, Interval) -> + Ids = [id_lookup(queue_stats, Obj) || Obj <- Objs], + DataLookup = get_data_from_nodes({rabbit_mgmt_data, all_list_queue_data, [Ids, Ranges]}), + adjust_hibernated_memory_use( + [begin + Id = id_lookup(queue_stats, Obj), + Pid = pget(pid, Obj), + QueueData = maps:get(Id, DataLookup), + Props = maps:get(queue_stats, QueueData), + Stats = queue_stats(QueueData, Ranges, Interval), + {Pid, combine(Props, Obj) ++ Stats} + end || Obj <- Objs]). + +detail_queue_stats(Ranges, Objs, Interval) -> + Ids = [id_lookup(queue_stats, Obj) || Obj <- Objs], + DataLookup = get_data_from_nodes({rabbit_mgmt_data, all_detail_queue_data, + [Ids, Ranges]}), + + QueueStats = adjust_hibernated_memory_use( + [begin + Id = id_lookup(queue_stats, Obj), + Pid = pget(pid, Obj), + QueueData = maps:get(Id, DataLookup), + Props = maps:get(queue_stats, QueueData), + Stats = queue_stats(QueueData, Ranges, Interval), + ConsumerStats = rabbit_mgmt_data_compat:fill_consumer_active_fields( + maps:get(consumer_stats, QueueData)), + Consumers = [{consumer_details, ConsumerStats}], + StatsD = [{deliveries, + detail_stats(QueueData, channel_queue_stats_deliver_stats, + deliver_get, second(Id), Ranges, Interval)}, + {incoming, + detail_stats(QueueData, queue_exchange_stats_publish, + fine_stats, first(Id), Ranges, Interval)}], + Details = augment_details(Obj, []), + {Pid, combine(Props, Obj) ++ Stats ++ StatsD ++ Consumers ++ Details} + end || Obj <- Objs]), + + % patch up missing channel details + ChPids = lists:usort(get_pids_for_missing_channel_details(QueueStats)), + ChDets = get_channel_detail_lookup(ChPids), + Merged = merge_channel_details(QueueStats, ChDets), + Merged. + +node_node_stats(Lookup, Node, Ranges, Interval) -> + LocalNodeNodeMetrics = maps:from_list(ets:tab2list(node_node_metrics)), + RemoteNodeNodeMetrics = maps:get(node_node_metrics, Lookup), + NodeNodeMetrics = maps:merge(LocalNodeNodeMetrics, RemoteNodeNodeMetrics), + node_node_stats(Lookup, Node, Ranges, Interval, NodeNodeMetrics). + +node_node_stats(Lookup, Node, Ranges, Interval, NodeNodeMetrics) -> + Table = node_node_coarse_stats, + Type = coarse_node_node_stats, + [begin + {Stats, DetailId} = get_detail_stats(Key, Lookup, Table, Type, + first(Node), Ranges, Interval), + NodeMetrics = maybe_fetch_value(Key, NodeNodeMetrics), + lists:flatten([{stats, Stats}, DetailId, NodeMetrics]) + end || {{T, Key}, _} <- maps:to_list(Lookup), T =:= Table]. + +detail_stats(Lookup, Table, Type, Id, Ranges, Interval) -> + [begin + {Stats, DetailId} = get_detail_stats(Key, Lookup, Table, Type, + Id, Ranges, Interval), + [{stats, Stats}|DetailId] %TODO: not actually delegated + end || {{T, Key}, _} <- maps:to_list(Lookup), T =:= Table]. + +get_detail_stats(Key, Lookup, Table, Type, Id, Ranges, Interval) -> + Range = pick_range(Type, Ranges), + Stats = format_range(Lookup, {Table, Key}, Range, Interval), + DetailId = format_detail_id(revert(Id, Key)), + {Stats, DetailId}. + +queue_stats(QueueData, Ranges, Interval) -> + message_stats(format_range(QueueData, queue_stats_publish, + pick_range(fine_stats, Ranges), Interval) ++ + format_range(QueueData, queue_stats_deliver_stats, + pick_range(deliver_get, Ranges), Interval)) ++ + format_range(QueueData, queue_process_stats, + pick_range(process_stats, Ranges), Interval) ++ + format_range(QueueData, queue_msg_stats, + pick_range(queue_msg_counts, Ranges), Interval). + +channel_stats(ChannelData, Ranges, Interval) -> + message_stats(format_range(ChannelData, channel_stats_fine_stats, + pick_range(fine_stats, Ranges), Interval) ++ + format_range(ChannelData, channel_stats_deliver_stats, + pick_range(deliver_get, Ranges), Interval)) ++ + format_range(ChannelData, channel_process_stats, + pick_range(process_stats, Ranges), Interval). + +-spec format_range(slide_data(), lookup_key(), maybe_range(), non_neg_integer()) -> + proplists:proplist(). +format_range(Data, Key, Range0, Interval) -> + Table = case Key of + {T, _} -> T; + T -> T + end, + InstantRateFun = fun() -> fetch_slides(1, Key, Data) end, + SamplesFun = fun() -> fetch_slides(2, Key, Data) end, + Now = exometer_slide:timestamp(), + rabbit_mgmt_stats:format_range(Range0, Now, Table, Interval, InstantRateFun, + SamplesFun). + +%% basic.get-empty metric +fetch_slides(Ele, Key, Data) + when Key =:= channel_queue_stats_deliver_stats orelse + Key =:= channel_stats_deliver_stats orelse + Key =:= queue_stats_deliver_stats orelse + Key =:= vhost_stats_deliver_stats orelse + (is_tuple(Key) andalso + (element(1, Key) =:= channel_queue_stats_deliver_stats orelse + element(1, Key) =:= channel_stats_deliver_stats orelse + element(1, Key) =:= queue_stats_deliver_stats orelse + element(1, Key) =:= vhost_stats_deliver_stats)) -> + case element(Ele, maps:get(Key, Data)) of + not_found -> []; + Slides when is_list(Slides) -> + [rabbit_mgmt_data_compat:fill_get_empty_queue_metric(S) + || S <- Slides, not_found =/= S]; + Slide -> + [rabbit_mgmt_data_compat:fill_get_empty_queue_metric(Slide)] + end; +%% drop_unroutable metric +fetch_slides(Ele, Key, Data) + when Key =:= channel_stats_fine_stats orelse + Key =:= channel_exchange_stats_fine_stats orelse + Key =:= vhost_stats_fine_stats orelse + (is_tuple(Key) andalso + (element(1, Key) =:= channel_stats_fine_stats orelse + element(1, Key) =:= channel_exchange_stats_fine_stats orelse + element(1, Key) =:= vhost_stats_fine_stats)) -> + case element(Ele, maps:get(Key, Data)) of + not_found -> []; + Slides when is_list(Slides) -> + [rabbit_mgmt_data_compat:fill_drop_unroutable_metric(S) + || S <- Slides, not_found =/= S]; + Slide -> + [rabbit_mgmt_data_compat:fill_drop_unroutable_metric(Slide)] + end; +fetch_slides(Ele, Key, Data) -> + case element(Ele, maps:get(Key, Data)) of + not_found -> []; + Slides when is_list(Slides) -> + [S || S <- Slides, not_found =/= S]; + Slide -> + [Slide] + end. + +get_channel_detail_lookup(ChPids) -> + ChDets = delegate_invoke({rabbit_mgmt_data, augment_channel_pids, [ChPids]}), + maps:from_list([{pget(pid, C), C} || [_|_] = C <- lists:append(ChDets)]). + +merge_channel_details(QueueStats, Lookup) -> + [begin + Cons = pget(consumer_details, QueueStat), + Cons1 = [merge_channel_into_obj(Con, Lookup) || Con <- Cons], + rabbit_misc:pset(consumer_details, Cons1, QueueStat) + end || QueueStat <- QueueStats]. + +merge_channel_into_obj(Obj, ChDet) -> + case pget(channel_details, Obj) of + [] -> case maps:find(pget(channel_pid, Obj), ChDet) of + {ok, CHd} -> + rabbit_misc:pset(channel_details, CHd, Obj); + error -> + Obj + end; + _ -> Obj + end. + +get_pids_for_missing_channel_details(QueueStats) -> + CDs = lists:append([pget(consumer_details, QueueStat) || QueueStat <- QueueStats]), + [ pget(channel_pid, CD) || CD <- CDs, [] =:= pget(channel_details, CD)]. + + +list_exchange_stats(Ranges, Objs, Interval) -> + Ids = [id_lookup(exchange_stats, Obj) || Obj <- Objs], + DataLookup = get_data_from_nodes({rabbit_mgmt_data, all_exchange_data, [Ids, Ranges]}), + [begin + Id = id_lookup(exchange_stats, Obj), + ExData = maps:get(Id, DataLookup), + Stats = message_stats(format_range(ExData, exchange_stats_publish_out, + pick_range(fine_stats, Ranges), Interval) ++ + format_range(ExData, exchange_stats_publish_in, + pick_range(deliver_get, Ranges), Interval)), + Obj ++ Stats + end || Obj <- Objs]. + +detail_exchange_stats(Ranges, Objs, Interval) -> + Ids = [id_lookup(exchange_stats, Obj) || Obj <- Objs], + DataLookup = get_data_from_nodes({rabbit_mgmt_data, all_exchange_data, [Ids, Ranges]}), + [begin + Id = id_lookup(exchange_stats, Obj), + ExData = maps:get(Id, DataLookup), + Stats = message_stats(format_range(ExData, exchange_stats_publish_out, + pick_range(fine_stats, Ranges), Interval) ++ + format_range(ExData, exchange_stats_publish_in, + pick_range(deliver_get, Ranges), Interval)), + StatsD = [{incoming, + detail_stats(ExData, channel_exchange_stats_fine_stats, + fine_stats, second(Id), Ranges, Interval)}, + {outgoing, + detail_stats(ExData, queue_exchange_stats_publish, + fine_stats, second(Id), Ranges, Interval)}], + %% remove live state? not sure it has! + Obj ++ StatsD ++ Stats + end || Obj <- Objs]. + +connection_stats(Ranges, Objs, Interval) -> + Ids = [id_lookup(connection_stats, Obj) || Obj <- Objs], + DataLookup = get_data_from_nodes({rabbit_mgmt_data, all_connection_data, [Ids, Ranges]}), + [begin + Id = id_lookup(connection_stats, Obj), + ConnData = maps:get(Id, DataLookup), + Props = maps:get(connection_stats, ConnData), + Stats = format_range(ConnData, connection_stats_coarse_conn_stats, + pick_range(coarse_conn_stats, Ranges), Interval), + Details = augment_details(Obj, []), % TODO: not delegated + combine(Props, Obj) ++ Details ++ Stats + end || Obj <- Objs]. + +list_channel_stats(Ranges, Objs, Interval) -> + Ids = [id_lookup(channel_stats, Obj) || Obj <- Objs], + DataLookup = get_data_from_nodes({rabbit_mgmt_data, all_list_channel_data, [Ids, Ranges]}), + ChannelStats = + [begin + Id = id_lookup(channel_stats, Obj), + ChannelData = maps:get(Id, DataLookup), + Props = maps:get(channel_stats, ChannelData), + Stats = channel_stats(ChannelData, Ranges, Interval), + combine(Props, Obj) ++ Stats + end || Obj <- Objs], + ChannelStats. + +detail_channel_stats(Ranges, Objs, Interval) -> + Ids = [id_lookup(channel_stats, Obj) || Obj <- Objs], + DataLookup = get_data_from_nodes({rabbit_mgmt_data, all_detail_channel_data, + [Ids, Ranges]}), + ChannelStats = + [begin + Id = id_lookup(channel_stats, Obj), + ChannelData = maps:get(Id, DataLookup), + Props = maps:get(channel_stats, ChannelData), + Stats = channel_stats(ChannelData, Ranges, Interval), + ConsumerStats = rabbit_mgmt_data_compat:fill_consumer_active_fields( + maps:get(consumer_stats, ChannelData)), + Consumers = [{consumer_details, ConsumerStats}], + StatsD = [{publishes, + detail_stats(ChannelData, channel_exchange_stats_fine_stats, + fine_stats, first(Id), Ranges, Interval)}, + {deliveries, + detail_stats(ChannelData, channel_queue_stats_deliver_stats, + fine_stats, first(Id), Ranges, Interval)}], + combine(Props, Obj) ++ Consumers ++ Stats ++ StatsD + end || Obj <- Objs], + rabbit_mgmt_format:strip_pids(ChannelStats). + +vhost_stats(Ranges, Objs, Interval) -> + Ids = [id_lookup(vhost_stats, Obj) || Obj <- Objs], + DataLookup = get_data_from_nodes({rabbit_mgmt_data, all_vhost_data, [Ids, Ranges]}), + [begin + Id = id_lookup(vhost_stats, Obj), + VData = maps:get(Id, DataLookup), + Stats = format_range(VData, vhost_stats_coarse_conn_stats, + pick_range(coarse_conn_stats, Ranges), Interval) ++ + format_range(VData, vhost_msg_stats, + pick_range(queue_msg_rates, Ranges), Interval), + StatsD = message_stats(format_range(VData, vhost_stats_fine_stats, + pick_range(fine_stats, Ranges), Interval) ++ + format_range(VData, vhost_stats_deliver_stats, + pick_range(deliver_get, Ranges), Interval)), + Details = augment_details(Obj, []), + Obj ++ Details ++ Stats ++ StatsD + end || Obj <- Objs]. + +node_stats(Ranges, Objs, Interval) -> + Ids = [id_lookup(node_stats, Obj) || Obj <- Objs], + DataLookup = get_data_from_nodes({rabbit_mgmt_data, all_node_data, [Ids, Ranges]}), + [begin + Id = id_lookup(node_stats, Obj), + NData = maps:get(Id, DataLookup), + Props = maps:get(node_stats, NData), + Stats = format_range(NData, node_coarse_stats, + pick_range(coarse_node_stats, Ranges), Interval) ++ + format_range(NData, node_persister_stats, + pick_range(coarse_node_stats, Ranges), Interval) ++ + format_range(NData, connection_churn_rates, + pick_range(churn_rates, Ranges), Interval), + NodeNodeStats = node_node_stats(NData, Id, Ranges, Interval), + StatsD = [{cluster_links, NodeNodeStats}], + MgmtStats = maps:get(mgmt_stats, NData), + Details = augment_details(Obj, []), % augmentation needs to be node local + combine(Props, Obj) ++ Details ++ Stats ++ StatsD ++ MgmtStats + end || Obj <- Objs]. + +combine(New, Old) -> + case pget(state, Old) of + unknown -> New ++ Old; + live -> New ++ lists:keydelete(state, 1, Old); + _ -> lists:keydelete(state, 1, New) ++ Old + end. + +revert({'_', _}, {Id, _}) -> + Id; +revert({_, '_'}, {_, Id}) -> + Id. + +%%---------------------------------------------------------------------------- +%% Internal, delegated operations +%%---------------------------------------------------------------------------- + +-spec get_data_from_nodes(mfargs()) -> #{atom() => any()}. +get_data_from_nodes(MFA) -> + Data = delegate_invoke(MFA), + lists:foldl(fun(D, Agg) -> + maps_merge(fun merge_data/3, D, Agg) + end, #{}, Data). + +maps_merge(Fun, M1, M2) -> + JustM2 = maps:without(maps:keys(M1), M2), + maps:merge(JustM2, + maps:map(fun(K, V1) -> + case maps:find(K, M2) of + {ok, V2} -> Fun(K, V1, V2); + error -> V1 + end + end, + M1)). + +-spec merge_data(atom(), any(), any()) -> any(). +merge_data(_, A, B) when is_integer(A), is_integer(B) -> A + B; +merge_data(_, A, B) when is_list(A), is_list(B) -> + A ++ B; +merge_data(_, {A1, B1}, {[_|_] = A2, [_|_] = B2}) -> + {[A1 | A2], [B1 | B2]}; +merge_data(_, {A1, B1}, {A2, B2}) -> % first slide + {[A1, A2], [B1, B2]}; +merge_data(_, D1, D2) -> % we assume if we get here both values a maps + try + maps_merge(fun merge_data/3, D1, D2) + catch + error:Err -> + rabbit_log:debug("merge_data err ~p got: ~p ~p ~n", [Err, D1, D2]), + case is_map(D1) of + true -> D1; + false -> D2 + end + end. + +%% We do this when retrieving the queue record rather than when +%% storing it since the memory use will drop *after* we find out about +%% hibernation, so to do it when we receive a queue stats event would +%% be fiddly and racy. This should be quite cheap though. +adjust_hibernated_memory_use(Qs) -> + Pids = [Pid || {Pid, Q} <- Qs, pget(idle_since, Q, not_idle) =/= not_idle], + %% We use delegate here not for ordering reasons but because we + %% want to get the right amount of parallelism and minimise + %% cross-cluster communication. + {Mem, _BadNodes} = delegate:invoke(Pids, {erlang, process_info, [memory]}), + MemDict = maps:from_list([{P, M} || {P, M = {memory, _}} <- Mem]), + [case maps:find(Pid, MemDict) of + error -> Q; + {ok, Memory} -> [Memory | proplists:delete(memory, Q)] + end || {Pid, Q} <- Qs]. + +-spec created_stats_delegated(any(), fun((any()) -> any()) | atom()) -> not_found | any(). +created_stats_delegated(Key, Type) -> + Data = delegate_invoke({rabbit_mgmt_data, augmented_created_stats, [Key, Type]}), + case [X || X <- Data, X =/= not_found] of + [] -> not_found; + [X] -> X + end. + +created_stats_delegated(Type) -> + lists:append( + delegate_invoke({rabbit_mgmt_data, augmented_created_stats, [Type]})). + +-spec delegate_invoke(mfargs()) -> [any()]. +delegate_invoke(FunOrMFA) -> + MemberPids = [P || P <- pg2:get_members(management_db)], + {Results, Errors} = delegate:invoke(MemberPids, ?DELEGATE_PREFIX, FunOrMFA), + case Errors of + [] -> ok; + _ -> rabbit_log:warning("Management delegate query returned errors:~n~p", [Errors]) + end, + [R || {_, R} <- Results]. + +submit(Fun) -> + {ok, Interval} = application:get_env(rabbit, collect_statistics_interval), + worker_pool:submit(management_worker_pool, fun() -> Fun(Interval) end, reuse). + +submit_cached(Key, Fun) -> + {ok, Interval} = application:get_env(rabbit, collect_statistics_interval), + {ok, Res} = rabbit_mgmt_db_cache:fetch(Key, fun() -> Fun(Interval) end), + Res. + +submit_cached(Key, Fun, Arg, Timeout) -> + {ok, Interval} = application:get_env(rabbit, collect_statistics_interval), + {ok, Res} = rabbit_mgmt_db_cache:fetch(Key, + fun(A) -> Fun(Interval, A) end, + [Arg], Timeout), + Res. + +%% Note: Assumes Key is a two-tuple. +%% If not found at first, Key is reversed and tried again. +%% Seems to be necessary based on node_node_metrics table keys +%% and Key values in Lookup +maybe_fetch_value(Key, Dict) -> + maybe_fetch_value(maps:is_key(Key, Dict), go, Key, Dict). + +maybe_fetch_value(true, _Cont, Key, Dict) -> + maps:get(Key, Dict); +maybe_fetch_value(false, stop, _Key, _Dict) -> + []; +maybe_fetch_value(false, go, Key, Dict) -> + Key2 = reverse_key(Key), + maybe_fetch_value(maps:is_key(Key2, Dict), stop, Key2, Dict). + +message_stats([]) -> + []; +message_stats(Stats) -> + [{message_stats, Stats}]. + +pick_range(Table, Ranges) -> + rabbit_mgmt_data:pick_range(Table, Ranges). + +first(Id) -> + {Id, '_'}. + +second(Id) -> + {'_', Id}. + +reverse_key({K1, K2}) -> + {K2, K1}. + +augment_details(Obj, Acc) -> + rabbit_mgmt_data:augment_details(Obj, Acc). + +format_detail_id(ChPid) when is_pid(ChPid) -> + augment_details([{channel, ChPid}], []); +format_detail_id(#resource{name = Name, virtual_host = Vhost, kind = Kind}) -> + [{Kind, [{name, Name}, {vhost, Vhost}]}]; +format_detail_id(Node) when is_atom(Node) -> + [{name, Node}]. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_db_cache.erl b/deps/rabbitmq_management/src/rabbit_mgmt_db_cache.erl new file mode 100644 index 0000000000..b4d92dd543 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_db_cache.erl @@ -0,0 +1,143 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2016-2020 VMware, Inc. or its affiliates. All rights reserved. + +-module(rabbit_mgmt_db_cache). + +-behaviour(gen_server). + +%% API functions +-export([start_link/1]). +-export([process_name/1, + fetch/2, + fetch/3, + fetch/4]). + +%% gen_server callbacks +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +-record(state, {data :: any() | none, + args :: [any()], + timer_ref :: undefined | reference(), + multiplier :: integer()}). + +-type error_desc() :: key_not_found | timeout | {throw, atom()}. +-type fetch_fun() :: fun((_) -> any()) | fun(() -> any()). +-type fetch_ret() :: {ok, any()} | {error, error_desc()}. + +-define(DEFAULT_MULT, 5). +-define(DEFAULT_TIMEOUT, 60000). +-define(CHILD(Key), {rabbit_mgmt_db_cache:process_name(Key), + {rabbit_mgmt_db_cache, start_link, [Key]}, + permanent, 5000, worker, + [rabbit_mgmt_db_cache]}). +-define(RESET_STATE(State), State#state{data = none, args = []}). + +%% Implements an adaptive cache that times the value generating fun +%% and uses the return value as the cached value for the time it took +%% to produce multiplied by some factor (defaults to 5). +%% There is one cache process per key. New processes are started as +%% required. The cache is invalidated if the arguments to the fetch +%% fun have changed. + + +%%%=================================================================== +%%% API functions +%%%=================================================================== + +-spec fetch(atom(), fetch_fun()) -> fetch_ret(). +fetch(Key, FetchFun) -> + fetch(Key, FetchFun, []). + +-spec fetch(atom(), fetch_fun(), [any()]) -> fetch_ret(). +fetch(Key, FetchFun, Args) when is_list(Args) -> + fetch(Key, FetchFun, Args, ?DEFAULT_TIMEOUT). + +-spec fetch(atom(), fetch_fun(), [any()], integer()) -> fetch_ret(). +fetch(Key, FetchFun, FunArgs, Timeout) -> + ProcName = process_name(Key), + Pid = case whereis(ProcName) of + undefined -> + {ok, P} = supervisor:start_child(rabbit_mgmt_db_cache_sup, + ?CHILD(Key)), + P; + P -> P + end, + gen_server:call(Pid, {fetch, FetchFun, FunArgs}, Timeout). + + +-spec process_name(atom()) -> atom(). +process_name(Key) -> + list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(Key)). + +-spec start_link(atom()) -> {ok, pid()} | ignore | {error, any()}. +start_link(Key) -> + gen_server:start_link({local, process_name(Key)}, ?MODULE, [], []). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +init([]) -> + Mult = application:get_env(rabbitmq_management, management_db_cache_multiplier, + ?DEFAULT_MULT), + {ok, #state{data = none, + args = [], + multiplier = Mult}}. + +handle_call({fetch, _FetchFun, FunArgs} = Msg, From, + #state{data = CachedData, args = Args} = State) when + CachedData =/= none andalso Args =/= FunArgs -> + %% there is cached data that needs to be invalidated + handle_call(Msg, From, ?RESET_STATE(State)); +handle_call({fetch, FetchFun, FunArgs}, _From, + #state{data = none, + multiplier = Mult, timer_ref = Ref} = State) -> + %% force a gc here to clean up previously cleared data + garbage_collect(), + case Ref of + R when is_reference(R) -> + _ = erlang:cancel_timer(R); + _ -> ok + end, + + try timer:tc(FetchFun, FunArgs) of + {Time, Data} -> + case trunc(Time / 1000 * Mult) of + 0 -> {reply, {ok, Data}, ?RESET_STATE(State)}; % no need to cache that + T -> + TimerRef = erlang:send_after(T, self(), purge_cache), + {reply, {ok, Data}, State#state{data = Data, + timer_ref = TimerRef, + args = FunArgs}} + end + catch + Throw -> {reply, {error, {throw, Throw}}, State} + end; +handle_call({fetch, _FetchFun, _}, _From, #state{data = Data} = State) -> + Reply = {ok, Data}, + {reply, Reply, State}; +handle_call(purge_cache, _From, State) -> + {reply, ok, ?RESET_STATE(State), hibernate}. + + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(purge_cache, State) -> + {noreply, ?RESET_STATE(State), hibernate}; +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_db_cache_sup.erl b/deps/rabbitmq_management/src/rabbit_mgmt_db_cache_sup.erl new file mode 100644 index 0000000000..1805f25872 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_db_cache_sup.erl @@ -0,0 +1,29 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2016-2020 VMware, Inc. or its affiliates. All rights reserved. + +-module(rabbit_mgmt_db_cache_sup). + +-behaviour(supervisor). + +%% API functions +-export([start_link/0]). + +%% Supervisor callbacks +-export([init/1]). + +%%%=================================================================== +%%% API functions +%%%=================================================================== + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%%%=================================================================== +%%% Supervisor callbacks +%%%=================================================================== + +init([]) -> + {ok, {{one_for_one, 5, 10}, []}}. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_dispatcher.erl b/deps/rabbitmq_management/src/rabbit_mgmt_dispatcher.erl new file mode 100644 index 0000000000..e0de5c8bce --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_dispatcher.erl @@ -0,0 +1,185 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_dispatcher). + +-export([modules/1, build_dispatcher/1]). + +-behaviour(rabbit_mgmt_extension). +-export([dispatcher/0, web_ui/0]). + +build_dispatcher(Ignore) -> + Routes = build_routes(Ignore), + cowboy_router:compile(Routes). + +build_routes(Ignore) -> + ManagementApp = module_app(?MODULE), + Prefix = rabbit_mgmt_util:get_path_prefix(), + RootIdxRtes = build_root_index_routes(Prefix, ManagementApp), + ApiRdrRte = build_redirect_route("/api", Prefix ++ "/api/index.html"), + CliRdrRte = build_redirect_route("/cli", Prefix ++ "/cli/index.html"), + StatsRdrRte1 = build_redirect_route("/stats", Prefix ++ "/api/index.html"), + StatsRdrRte2 = build_redirect_route("/doc/stats.html", Prefix ++ "/api/index.html"), + MgmtRdrRte = {"/mgmt", rabbit_mgmt_wm_redirect, "/"}, + LocalPaths = [{module_app(M), "www"} || M <- modules(Ignore)], + LocalStaticRte = {"/[...]", rabbit_mgmt_wm_static, LocalPaths}, + % NB: order is significant in the routing list + Routes0 = build_module_routes(Ignore) ++ + [ApiRdrRte, CliRdrRte, MgmtRdrRte, StatsRdrRte1, StatsRdrRte2, LocalStaticRte], + Routes1 = maybe_add_path_prefix(Routes0, Prefix), + % NB: ensure the root routes are first + Routes2 = RootIdxRtes ++ Routes1, + [{'_', Routes2}]. + +build_root_index_routes("", ManagementApp) -> + [{"/", rabbit_mgmt_wm_static, root_idx_file(ManagementApp)}]; +build_root_index_routes(Prefix, ManagementApp) -> + [{"/", rabbit_mgmt_wm_redirect, Prefix ++ "/"}, + {Prefix, rabbit_mgmt_wm_static, root_idx_file(ManagementApp)}]. + +build_redirect_route(Path, Location) -> + {Path, rabbit_mgmt_wm_redirect, Location}. + +root_idx_file(ManagementApp) -> + {priv_file, ManagementApp, "www/index.html"}. + +maybe_add_path_prefix(Routes, "") -> + Routes; +maybe_add_path_prefix(Routes, Prefix) -> + [{Prefix ++ Path, Mod, Args} || {Path, Mod, Args} <- Routes]. + +build_module_routes(Ignore) -> + Routes = [Module:dispatcher() || Module <- modules(Ignore)], + [{"/api" ++ Path, Mod, Args} || {Path, Mod, Args} <- lists:append(Routes)]. + +modules(IgnoreApps) -> + [Module || {App, Module, Behaviours} <- + %% Sort rabbitmq_management modules first. This is + %% a microoptimization because most files belong to + %% this application. Making it first avoids several + %% stats(2) which have a great chance of failing in other + %% applications. + lists:sort( + fun + ({rabbitmq_management, _, _}, _) -> true; + (_, {rabbitmq_management, _, _}) -> false; + ({A, _, _}, {B, _, _}) -> A =< B + end, + rabbit_misc:all_module_attributes(behaviour)), + not lists:member(App, IgnoreApps), + lists:member(rabbit_mgmt_extension, Behaviours)]. + +module_app(Module) -> + {ok, App} = application:get_application(Module), + App. + +%%---------------------------------------------------------------------------- + +web_ui() -> [{javascript, <<"dispatcher.js">>}]. + +dispatcher() -> + [{"/overview", rabbit_mgmt_wm_overview, []}, + {"/cluster-name", rabbit_mgmt_wm_cluster_name, []}, + {"/nodes", rabbit_mgmt_wm_nodes, []}, + {"/nodes/:node", rabbit_mgmt_wm_node, []}, + {"/nodes/:node/memory", rabbit_mgmt_wm_node_memory, [absolute]}, + {"/nodes/:node/memory/relative", rabbit_mgmt_wm_node_memory, [relative]}, + {"/nodes/:node/memory/ets", rabbit_mgmt_wm_node_memory_ets, [absolute]}, + {"/nodes/:node/memory/ets/relative", rabbit_mgmt_wm_node_memory_ets, [relative]}, + {"/nodes/:node/memory/ets/:filter", rabbit_mgmt_wm_node_memory_ets, [absolute]}, + {"/nodes/:node/memory/ets/:filter/relative", rabbit_mgmt_wm_node_memory_ets, [relative]}, + {"/extensions", rabbit_mgmt_wm_extensions, []}, + {"/all-configuration", rabbit_mgmt_wm_definitions, []}, %% This was the old name, let's not break things gratuitously. + {"/definitions", rabbit_mgmt_wm_definitions, []}, + {"/definitions/:vhost", rabbit_mgmt_wm_definitions, []}, + {"/parameters", rabbit_mgmt_wm_parameters, []}, + {"/parameters/:component", rabbit_mgmt_wm_parameters, []}, + {"/parameters/:component/:vhost", rabbit_mgmt_wm_parameters, []}, + {"/parameters/:component/:vhost/:name", rabbit_mgmt_wm_parameter, []}, + {"/global-parameters", rabbit_mgmt_wm_global_parameters, []}, + {"/global-parameters/:name", rabbit_mgmt_wm_global_parameter, []}, + {"/policies", rabbit_mgmt_wm_policies, []}, + {"/policies/:vhost", rabbit_mgmt_wm_policies, []}, + {"/policies/:vhost/:name", rabbit_mgmt_wm_policy, []}, + {"/operator-policies", rabbit_mgmt_wm_operator_policies, []}, + {"/operator-policies/:vhost", rabbit_mgmt_wm_operator_policies, []}, + {"/operator-policies", rabbit_mgmt_wm_operator_policies, []}, + {"/operator-policies/:vhost/:name", rabbit_mgmt_wm_operator_policy, []}, + {"/vhost-limits/:vhost/:name", rabbit_mgmt_wm_limit, []}, + {"/vhost-limits", rabbit_mgmt_wm_limits, []}, + {"/vhost-limits/:vhost", rabbit_mgmt_wm_limits, []}, + {"/connections", rabbit_mgmt_wm_connections, []}, + {"/connections/:connection", rabbit_mgmt_wm_connection, []}, + {"/connections/:connection/channels", rabbit_mgmt_wm_connection_channels, []}, + {"/channels", rabbit_mgmt_wm_channels, []}, + {"/channels/:channel", rabbit_mgmt_wm_channel, []}, + {"/consumers", rabbit_mgmt_wm_consumers, []}, + {"/consumers/:vhost", rabbit_mgmt_wm_consumers, []}, + {"/exchanges", rabbit_mgmt_wm_exchanges, []}, + {"/exchanges/:vhost", rabbit_mgmt_wm_exchanges, []}, + {"/exchanges/:vhost/:exchange", rabbit_mgmt_wm_exchange, []}, + {"/exchanges/:vhost/:exchange/publish", rabbit_mgmt_wm_exchange_publish, []}, + {"/exchanges/:vhost/:exchange/bindings/source", rabbit_mgmt_wm_bindings, [exchange_source]}, + {"/exchanges/:vhost/:exchange/bindings/destination", rabbit_mgmt_wm_bindings, [exchange_destination]}, + {"/queues", rabbit_mgmt_wm_queues, []}, + {"/queues/:vhost", rabbit_mgmt_wm_queues, []}, + {"/queues/:vhost/:queue", rabbit_mgmt_wm_queue, []}, + {"/queues/:vhost/:destination/bindings", rabbit_mgmt_wm_bindings, [queue]}, + {"/queues/:vhost/:queue/contents", rabbit_mgmt_wm_queue_purge, []}, + {"/queues/:vhost/:queue/get", rabbit_mgmt_wm_queue_get, []}, + {"/queues/:vhost/:queue/actions", rabbit_mgmt_wm_queue_actions, []}, + {"/bindings", rabbit_mgmt_wm_bindings, [all]}, + {"/bindings/:vhost", rabbit_mgmt_wm_bindings, [all]}, + {"/bindings/:vhost/e/:source/:dtype/:destination", rabbit_mgmt_wm_bindings, [source_destination]}, + {"/bindings/:vhost/e/:source/:dtype/:destination/:props", rabbit_mgmt_wm_binding, []}, + {"/vhosts", rabbit_mgmt_wm_vhosts, []}, + {"/vhosts/:vhost", rabbit_mgmt_wm_vhost, []}, + {"/vhosts/:vhost/start/:node", rabbit_mgmt_wm_vhost_restart, []}, + {"/vhosts/:vhost/permissions", rabbit_mgmt_wm_permissions_vhost, []}, + {"/vhosts/:vhost/topic-permissions", rabbit_mgmt_wm_topic_permissions_vhost, []}, + %% /connections/:connection is already taken, we cannot use our standard scheme here + {"/vhosts/:vhost/connections", rabbit_mgmt_wm_connections_vhost, []}, + %% /channels/:channel is already taken, we cannot use our standard scheme here + {"/vhosts/:vhost/channels", rabbit_mgmt_wm_channels_vhost, []}, + {"/users/bulk-delete", rabbit_mgmt_wm_users_bulk_delete, []}, + {"/users/without-permissions", rabbit_mgmt_wm_users, [without_permissions]}, + {"/users", rabbit_mgmt_wm_users, [all]}, + {"/users/:user", rabbit_mgmt_wm_user, []}, + {"/users/:user/permissions", rabbit_mgmt_wm_permissions_user, []}, + {"/users/:user/topic-permissions", rabbit_mgmt_wm_topic_permissions_user, []}, + {"/user-limits/:user/:name", rabbit_mgmt_wm_user_limit, []}, + {"/user-limits", rabbit_mgmt_wm_user_limits, []}, + {"/user-limits/:user", rabbit_mgmt_wm_user_limits, []}, + {"/feature-flags", rabbit_mgmt_wm_feature_flags, []}, + {"/feature-flags/:name/enable", rabbit_mgmt_wm_feature_flag_enable, []}, + {"/whoami", rabbit_mgmt_wm_whoami, []}, + {"/permissions", rabbit_mgmt_wm_permissions, []}, + {"/permissions/:vhost/:user", rabbit_mgmt_wm_permission, []}, + {"/topic-permissions", rabbit_mgmt_wm_topic_permissions, []}, + {"/topic-permissions/:vhost/:user", rabbit_mgmt_wm_topic_permission, []}, + {"/topic-permissions/:vhost/:user/:exchange", rabbit_mgmt_wm_topic_permission, []}, + {"/aliveness-test/:vhost", rabbit_mgmt_wm_aliveness_test, []}, + %% now deprecated + {"/healthchecks/node", rabbit_mgmt_wm_healthchecks, []}, + {"/healthchecks/node/:node", rabbit_mgmt_wm_healthchecks, []}, + %% modern generation of fine-grained health checks + {"/health/checks/alarms", rabbit_mgmt_wm_health_check_alarms, []}, + {"/health/checks/local-alarms", rabbit_mgmt_wm_health_check_local_alarms, []}, + {"/health/checks/certificate-expiration/:within/:unit", rabbit_mgmt_wm_health_check_certificate_expiration, []}, + {"/health/checks/port-listener/:port", rabbit_mgmt_wm_health_check_port_listener, []}, + {"/health/checks/protocol-listener/:protocol", rabbit_mgmt_wm_health_check_protocol_listener, []}, + {"/health/checks/virtual-hosts", rabbit_mgmt_wm_health_check_virtual_hosts, []}, + {"/health/checks/node-is-mirror-sync-critical", rabbit_mgmt_wm_health_check_node_is_mirror_sync_critical, []}, + {"/health/checks/node-is-quorum-critical", rabbit_mgmt_wm_health_check_node_is_quorum_critical, []}, + {"/reset", rabbit_mgmt_wm_reset, []}, + {"/reset/:node", rabbit_mgmt_wm_reset, []}, + {"/rebalance/queues", rabbit_mgmt_wm_rebalance_queues, [{queues, all}]}, + {"/auth", rabbit_mgmt_wm_auth, []}, + {"/auth/attempts/:node", rabbit_mgmt_wm_auth_attempts, [all]}, + {"/auth/attempts/:node/source", rabbit_mgmt_wm_auth_attempts, [by_source]}, + {"/login", rabbit_mgmt_wm_login, []} + ]. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_extension.erl b/deps/rabbitmq_management/src/rabbit_mgmt_extension.erl new file mode 100644 index 0000000000..1ee72aaf36 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_extension.erl @@ -0,0 +1,16 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2011-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_extension). + +%% Return a Cowboy dispatcher table to integrate +-callback dispatcher() -> [{string(), atom(), [atom()]}]. + +%% Return a proplist of information for the web UI to integrate +%% this extension. Currently the proplist should have one key, +%% 'javascript', the name of a javascript file to load and run. +-callback web_ui() -> proplists:proplist(). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_headers.erl b/deps/rabbitmq_management/src/rabbit_mgmt_headers.erl new file mode 100644 index 0000000000..89bb464c01 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_headers.erl @@ -0,0 +1,34 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +%% This module contains helper functions that control +%% response headers related to CORS, CSP, HSTS and so on. +-module(rabbit_mgmt_headers). + +-export([set_common_permission_headers/2]). +-export([set_cors_headers/2, set_hsts_headers/2, set_csp_headers/2]). + +%% +%% API +%% + +set_cors_headers(ReqData, Module) -> + rabbit_mgmt_cors:set_headers(ReqData, Module). + +set_hsts_headers(ReqData, _Module) -> + rabbit_mgmt_hsts:set_headers(ReqData). + +set_csp_headers(ReqData, _Module) -> + rabbit_mgmt_csp:set_headers(ReqData). + +set_common_permission_headers(ReqData0, EndpointModule) -> + lists:foldl(fun(Fun, ReqData) -> + Fun(ReqData, EndpointModule) + end, ReqData0, + [fun set_csp_headers/2, + fun set_hsts_headers/2, + fun set_cors_headers/2]). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_hsts.erl b/deps/rabbitmq_management/src/rabbit_mgmt_hsts.erl new file mode 100644 index 0000000000..004f3d4e7e --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_hsts.erl @@ -0,0 +1,28 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +%% Sets HSTS header(s) on the response if configured, +%% see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security. + +-module(rabbit_mgmt_hsts). + +-export([set_headers/1]). + +-define(HSTS_HEADER, <<"strict-transport-security">>). + +%% +%% API +%% + +set_headers(ReqData) -> + case application:get_env(rabbitmq_management, strict_transport_security) of + undefined -> ReqData; + {ok, Value} -> + cowboy_req:set_resp_header(?HSTS_HEADER, + rabbit_data_coercion:to_binary(Value), + ReqData) + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_load_definitions.erl b/deps/rabbitmq_management/src/rabbit_mgmt_load_definitions.erl new file mode 100644 index 0000000000..ba9962d6cf --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_load_definitions.erl @@ -0,0 +1,27 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_load_definitions). + +-include_lib("rabbit_common/include/rabbit.hrl"). + +-export([boot/0, maybe_load_definitions/0, maybe_load_definitions_from/2]). + +%% This module exists for backwards compatibility only. +%% Definition import functionality is now a core server feature. + +boot() -> + rabbit_log:debug("Will import definitions file from management.load_definitions"), + rabbit_definitions:maybe_load_definitions(rabbitmq_management, load_definitions). + +maybe_load_definitions() -> + rabbit_definitions:maybe_load_definitions(). + +maybe_load_definitions_from(true, Dir) -> + rabbit_definitions:maybe_load_definitions_from(true, Dir); +maybe_load_definitions_from(false, File) -> + rabbit_definitions:maybe_load_definitions_from(false, File). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_reset_handler.erl b/deps/rabbitmq_management/src/rabbit_mgmt_reset_handler.erl new file mode 100644 index 0000000000..4eb0219e46 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_reset_handler.erl @@ -0,0 +1,79 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +%% When management extensions are enabled and/or disabled at runtime, the +%% management web dispatch mechanism needs to be reset. This event handler +%% deals with responding to 'plugins_changed' events for management +%% extensions, forcing a reset when necessary. + +-module(rabbit_mgmt_reset_handler). + +-include_lib("rabbit_common/include/rabbit.hrl"). + +-behaviour(gen_event). + +-export([init/1, handle_call/2, handle_event/2, handle_info/2, + terminate/2, code_change/3]). + +-rabbit_boot_step({?MODULE, + [{description, "management extension handling"}, + {mfa, {gen_event, add_handler, + [rabbit_event, ?MODULE, []]}}, + {cleanup, {gen_event, delete_handler, + [rabbit_event, ?MODULE, []]}}, + {requires, rabbit_event}, + {enables, recovery}]}). + +-import(rabbit_misc, [pget/2, pget/3]). + +%%---------------------------------------------------------------------------- + +init([]) -> + {ok, []}. + +handle_call(_Request, State) -> + {ok, not_understood, State}. + +handle_event(#event{type = plugins_changed, props = Details}, State) -> + Enabled = pget(enabled, Details), + Disabled = pget(disabled, Details), + case extensions_changed(Enabled ++ Disabled) of + true -> + _ = rabbit_mgmt_app:reset_dispatcher(Disabled), + ok; + false -> ok + end, + {ok, State}; + +handle_event(_Event, State) -> + {ok, State}. + +handle_info(_Info, State) -> + {ok, State}. + +terminate(_Arg, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%---------------------------------------------------------------------------- + +%% We explicitly ignore the case where management has been +%% started/stopped since the dispatcher is either freshly created or +%% about to vanish. +extensions_changed(Apps) -> + not lists:member(rabbitmq_management, Apps) andalso + lists:any(fun is_extension/1, [Mod || App <- Apps, Mod <- mods(App)]). + +is_extension(Mod) -> + lists:member(rabbit_mgmt_extension, + pget(behaviour, Mod:module_info(attributes), [])). + +mods(App) -> + {ok, Modules} = application:get_key(App, modules), + Modules. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_stats.erl b/deps/rabbitmq_management/src/rabbit_mgmt_stats.erl new file mode 100644 index 0000000000..14b3432790 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_stats.erl @@ -0,0 +1,739 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2010-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_stats). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_metrics.hrl"). + +-export([format_range/6]). + +-define(MICRO_TO_MILLI, 1000). + +-type maybe_range() :: no_range | #range{}. +-type maybe_slide() :: exometer_slide:slide() | not_found. +-type maybe_slide_fun() :: fun(() -> [maybe_slide()]). + +-export_type([maybe_range/0, maybe_slide/0, maybe_slide_fun/0]). + + +%%---------------------------------------------------------------------------- +%% Query-time +%%---------------------------------------------------------------------------- + +-spec format_range(maybe_range(), exometer_slide:timestamp(), atom(), + non_neg_integer(), maybe_slide_fun(), maybe_slide_fun()) -> + proplists:proplist(). +format_range(no_range, Now, Table, Interval, InstantRateFun, _SamplesFun) -> + format_no_range(Table, Now, Interval, InstantRateFun); +format_range(#range{last = Last, first = First, incr = Incr}, _Now, Table, + Interval, _InstantRateFun, SamplesFun) -> + case SamplesFun() of + [] -> + []; + Slides -> + Empty = empty(Table, 0), + Slide = exometer_slide:sum(Last, First, Incr, Slides, Empty), + List = exometer_slide:to_list(Last, First, Slide), + Length = length(List), + RangePoint = Last - Interval, + LastTwo = get_last_two(List, Length), + {Total, Rate} = calculate_rate(LastTwo, Table, RangePoint), + {Samples, SampleTotals} = format_samples(List, empty(Table, []), + Empty), + format_rate(Table, Total, Rate, Samples, SampleTotals, Length) + end. + +-spec format_no_range(atom(), exometer_slide:timestamp(), non_neg_integer(), + maybe_slide_fun()) -> proplists:proplist(). +format_no_range(Table, Now, Interval, InstantRateFun) -> + RangePoint = ((Now div Interval) * Interval) - Interval, + case calculate_instant_rate(InstantRateFun, Table, RangePoint) of + {Total, Rate} -> format_rate(Table, Total, Rate); + not_found -> [] + end. + +-spec calculate_instant_rate(maybe_slide_fun(), atom(), integer()) -> + 'not_found' | {integer(), number()} | {tuple(), tuple()}. +calculate_instant_rate(Fun, Table, RangePoint) -> + case Fun() of + [] -> + not_found; + Slides -> + Slide = exometer_slide:sum(Slides), + case exometer_slide:last_two(Slide) of + [] -> {empty(Table, 0), empty(Table, 0.0)}; + [Last | T] -> + Total = get_total(Slide, Table), + Rate = rate_from_last_increment(Table, Last, T, RangePoint), + {Total, Rate} + end + end. + +calculate_rate([], Table, _RangePoint) -> + {empty(Table, 0), empty(Table, 0.0)}; +calculate_rate([{_, Total} = Last | T], Table, RangePoint) -> + Rate = rate_from_last_increment(Table, Last, T, RangePoint), + {Total, Rate}. + +get_last_two(List, Length) when Length =< 2 -> + lists:reverse(List); +get_last_two(List, Length) -> + lists:reverse(lists:nthtail(Length - 2, List)). + +get_total(Slide, Table) -> + case exometer_slide:last(Slide) of + undefined -> empty(Table, 0); + Other -> Other + end. + +format_samples(Samples, ESamples, ETotal) -> + lists:foldl(fun({TS, Sample}, {SamplesAcc, TotalsAcc}) -> + append_full_sample(TS, Sample, SamplesAcc, TotalsAcc) + end, {ESamples, ETotal}, Samples). + +%% Ts for "totals", Ss for "samples", Vs for "values" +%% +%% connection_stats_coarse_conn_stats, channel_stats_fine_stats, +%% vhost_stats_fine_stats, queue_msg_stats, vhost_msg_stats +append_full_sample(TS, {V1, V2, V3}, {S1, S2, S3}, {T1, T2, T3}) -> + {{append_sample(V1, TS, S1), append_sample(V2, TS, S2), append_sample(V3, TS, S3)}, + {V1 + T1, V2 + T2, V3 + T3}}; +%% channel_exchange_stats_fine_stats +append_full_sample(TS, {V1, V2, V3, V4}, {S1, S2, S3, S4}, {T1, T2, T3, T4}) -> + {{append_sample(V1, TS, S1), append_sample(V2, TS, S2), append_sample(V3, TS, S3), + append_sample(V4, TS, S4)}, + {V1 + T1, V2 + T2, V3 + T3, V4 + T4}}; +%% connection_churn_rates +append_full_sample(TS, {V1, V2, V3, V4, V5, V6, V7}, + {S1, S2, S3, S4, S5, S6, S7}, + {T1, T2, T3, T4, T5, T6, T7}) -> + {{append_sample(V1, TS, S1), append_sample(V2, TS, S2), + append_sample(V3, TS, S3), append_sample(V4, TS, S4), + append_sample(V5, TS, S5), append_sample(V6, TS, S6), + append_sample(V7, TS, S7)}, + {V1 + T1, V2 + T2, V3 + T3, V4 + T4, V5 + T5, V6 + T6, V7 + T7}}; +%% channel_queue_stats_deliver_stats, queue_stats_deliver_stats, +%% vhost_stats_deliver_stats, channel_stats_deliver_stats +%% node_coarse_stats +append_full_sample(TS, {V1, V2, V3, V4, V5, V6, V7, V8}, + {S1, S2, S3, S4, S5, S6, S7, S8}, + {T1, T2, T3, T4, T5, T6, T7, T8}) -> + {{append_sample(V1, TS, S1), append_sample(V2, TS, S2), + append_sample(V3, TS, S3), append_sample(V4, TS, S4), + append_sample(V5, TS, S5), append_sample(V6, TS, S6), + append_sample(V7, TS, S7), append_sample(V8, TS, S8)}, + {V1 + T1, V2 + T2, V3 + T3, V4 + T4, V5 + T5, V6 + T6, V7 + T7, V8 + T8}}; +%% channel_process_stats, queue_stats_publish, queue_exchange_stats_publish, +%% exchange_stats_publish_out, exchange_stats_publish_in, queue_process_stats +append_full_sample(TS, {V1}, {S1}, {T1}) -> + {{append_sample(V1, TS, S1)}, {V1 + T1}}; +%% node_persister_stats +append_full_sample(TS, + {V1, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12, V13, V14, + V15, V16, V17, V18, V19, V20}, + {S1, S2, S3, S4, S5, S6, S7, S8, S9, S10, S11, S12, S13, S14, + S15, S16, S17, S18, S19, S20}, + {T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, + T15, T16, T17, T18, T19, T20} + ) -> + {{append_sample(V1, TS, S1), append_sample(V2, TS, S2), + append_sample(V3, TS, S3), append_sample(V4, TS, S4), + append_sample(V5, TS, S5), append_sample(V6, TS, S6), + append_sample(V7, TS, S7), append_sample(V8, TS, S8), + append_sample(V9, TS, S9), append_sample(V10, TS, S10), + append_sample(V11, TS, S11), append_sample(V12, TS, S12), + append_sample(V13, TS, S13), append_sample(V14, TS, S14), + append_sample(V15, TS, S15), append_sample(V16, TS, S16), + append_sample(V17, TS, S17), append_sample(V18, TS, S18), + append_sample(V19, TS, S19), append_sample(V20, TS, S20)}, + {V1 + T1, V2 + T2, V3 + T3, V4 + T4, V5 + T5, V6 + T6, V7 + T7, V8 + T8, + V9 + T9, V10 + T10, V11 + T11, V12 + T12, V13 + T13, V14 + T14, V15 + T15, + V16 + T16, V17 + T17, V18 + T18, V19 + T19, V20 + T20}}; +%% node_node_coarse_stats, vhost_stats_coarse_connection_stats, queue_msg_rates, +%% vhost_msg_rates +append_full_sample(TS, {V1, V2}, {S1, S2}, {T1, T2}) -> + {{append_sample(V1, TS, S1), append_sample(V2, TS, S2)}, {V1 + T1, V2 + T2}}. + + +format_rate(connection_stats_coarse_conn_stats, {TR, TS, TRe}, {RR, RS, RRe}) -> + [ + {send_oct, TS}, + {send_oct_details, [{rate, RS}]}, + {recv_oct, TR}, + {recv_oct_details, [{rate, RR}]}, + {reductions, TRe}, + {reductions_details, [{rate, RRe}]} + ]; +format_rate(vhost_stats_coarse_conn_stats, {TR, TS}, {RR, RS}) -> + [ + {send_oct, TS}, + {send_oct_details, [{rate, RS}]}, + {recv_oct, TR}, + {recv_oct_details, [{rate, RR}]} + ]; +format_rate(Type, {TR, TW}, {RR, RW}) when Type =:= vhost_msg_rates; + Type =:= queue_msg_rates -> + [ + {disk_reads, TR}, + {disk_reads_details, [{rate, RR}]}, + {disk_writes, TW}, + {disk_writes_details, [{rate, RW}]} + ]; +format_rate(Type, {TP, TC, TRe, TDrop}, {RP, RC, RRe, RDrop}) + when Type =:= channel_stats_fine_stats; + Type =:= vhost_stats_fine_stats; + Type =:= channel_exchange_stats_fine_stats -> + [ + {publish, TP}, + {publish_details, [{rate, RP}]}, + {confirm, TC}, + {confirm_details, [{rate, RC}]}, + {return_unroutable, TRe}, + {return_unroutable_details, [{rate, RRe}]}, + {drop_unroutable, TDrop}, + {drop_unroutable_details, [{rate, RDrop}]} + ]; +format_rate(Type, {TG, TGN, TD, TDN, TR, TA, TDG, TGE}, + {RG, RGN, RD, RDN, RR, RA, RDG, RGE}) + when Type =:= channel_queue_stats_deliver_stats; + Type =:= channel_stats_deliver_stats; + Type =:= vhost_stats_deliver_stats; + Type =:= queue_stats_deliver_stats -> + [ + {get, TG}, + {get_details, [{rate, RG}]}, + {get_no_ack, TGN}, + {get_no_ack_details, [{rate, RGN}]}, + {deliver, TD}, + {deliver_details, [{rate, RD}]}, + {deliver_no_ack, TDN}, + {deliver_no_ack_details, [{rate, RDN}]}, + {redeliver, TR}, + {redeliver_details, [{rate, RR}]}, + {ack, TA}, + {ack_details, [{rate, RA}]}, + {deliver_get, TDG}, + {deliver_get_details, [{rate, RDG}]}, + {get_empty, TGE}, + {get_empty_details, [{rate, RGE}]} + ]; +format_rate(Type, {TR}, {RR}) when Type =:= channel_process_stats; + Type =:= queue_process_stats -> + [ + {reductions, TR}, + {reductions_details, [{rate, RR}]} + ]; +format_rate(exchange_stats_publish_out, {TP}, {RP}) -> + [ + {publish_out, TP}, + {publish_out_details, [{rate, RP}]} + ]; +format_rate(exchange_stats_publish_in, {TP}, {RP}) -> + [ + {publish_in, TP}, + {publish_in_details, [{rate, RP}]} + ]; +format_rate(Type, {TP}, {RP}) when Type =:= queue_stats_publish; + Type =:= queue_exchange_stats_publish -> + [ + {publish, TP}, + {publish_details, [{rate, RP}]} + ]; +format_rate(Type, {TR, TU, TM}, {RR, RU, RM}) when Type =:= queue_msg_stats; + Type =:= vhost_msg_stats -> + [ + {messages_ready, TR}, + {messages_ready_details, [{rate, RR}]}, + {messages_unacknowledged, TU}, + {messages_unacknowledged_details, [{rate, RU}]}, + {messages, TM}, + {messages_details, [{rate, RM}]} + ]; +format_rate(node_coarse_stats, {TF, TS, TM, TD, TP, TGC, TGCW, TCS}, + {RF, RS, RM, RD, RP, RGC, RGCW, RCS}) -> + [ + {mem_used, TM}, + {mem_used_details, [{rate, RM}]}, + {fd_used, TF}, + {fd_used_details, [{rate, RF}]}, + {sockets_used, TS}, + {sockets_used_details, [{rate, RS}]}, + {proc_used, TP}, + {proc_used_details, [{rate, RP}]}, + {disk_free, TD}, + {disk_free_details, [{rate, RD}]}, + {gc_num, TGC}, + {gc_num_details, [{rate, RGC}]}, + {gc_bytes_reclaimed, TGCW}, + {gc_bytes_reclaimed_details, [{rate, RGCW}]}, + {context_switches, TCS}, + {context_switches_details, [{rate, RCS}]} + ]; +format_rate(node_persister_stats, + {TIR, TIB, TIA, TIWC, TIWB, TIWAT, TIS, TISAT, TISC, + TISEAT, TIRC, TMRTC, TMDTC, TMSRC, TMSWC, TQIJWC, TQIWC, TQIRC, + TIO, TIOAT}, + {RIR, RIB, RIA, RIWC, RIWB, RIWAT, RIS, RISAT, RISC, + RISEAT, RIRC, RMRTC, RMDTC, RMSRC, RMSWC, RQIJWC, RQIWC, RQIRC, + RIO, RIOAT}) -> + %% Calculates average times for read/write/sync/seek from the + %% accumulated time and count + %% io_<op>_avg_time is the average operation time for the life of the node + %% io_<op>_avg_time_details/rate is the average operation time during the + %% last time unit calculated (thus similar to an instant rate) + [ + {io_read_count, TIR}, + {io_read_count_details, [{rate, RIR}]}, + {io_read_bytes, TIB}, + {io_read_bytes_details, [{rate, RIB}]}, + {io_read_avg_time, avg_time(TIA, TIR)}, + {io_read_avg_time_details, [{rate, avg_time(RIA, RIR)}]}, + {io_write_count, TIWC}, + {io_write_count_details, [{rate, RIWC}]}, + {io_write_bytes, TIWB}, + {io_write_bytes_details, [{rate, RIWB}]}, + {io_write_avg_time, avg_time(TIWAT, TIWC)}, + {io_write_avg_time_details, [{rate, avg_time(RIWAT, RIWC)}]}, + {io_sync_count, TIS}, + {io_sync_count_details, [{rate, RIS}]}, + {io_sync_avg_time, avg_time(TISAT, TIS)}, + {io_sync_avg_time_details, [{rate, avg_time(RISAT, RIS)}]}, + {io_seek_count, TISC}, + {io_seek_count_details, [{rate, RISC}]}, + {io_seek_avg_time, avg_time(TISEAT, TISC)}, + {io_seek_avg_time_details, [{rate, avg_time(RISEAT, RISC)}]}, + {io_reopen_count, TIRC}, + {io_reopen_count_details, [{rate, RIRC}]}, + {mnesia_ram_tx_count, TMRTC}, + {mnesia_ram_tx_count_details, [{rate, RMRTC}]}, + {mnesia_disk_tx_count, TMDTC}, + {mnesia_disk_tx_count_details, [{rate, RMDTC}]}, + {msg_store_read_count, TMSRC}, + {msg_store_read_count_details, [{rate, RMSRC}]}, + {msg_store_write_count, TMSWC}, + {msg_store_write_count_details, [{rate, RMSWC}]}, + {queue_index_journal_write_count, TQIJWC}, + {queue_index_journal_write_count_details, [{rate, RQIJWC}]}, + {queue_index_write_count, TQIWC}, + {queue_index_write_count_details, [{rate, RQIWC}]}, + {queue_index_read_count, TQIRC}, + {queue_index_read_count_details, [{rate, RQIRC}]}, + {io_file_handle_open_attempt_count, TIO}, + {io_file_handle_open_attempt_count_details, [{rate, RIO}]}, + {io_file_handle_open_attempt_avg_time, avg_time(TIOAT, TIO)}, + {io_file_handle_open_attempt_avg_time_details, [{rate, avg_time(RIOAT, RIO)}]} + ]; +format_rate(node_node_coarse_stats, {TS, TR}, {RS, RR}) -> + [ + {send_bytes, TS}, + {send_bytes_details, [{rate, RS}]}, + {recv_bytes, TR}, + {recv_bytes_details, [{rate, RR}]} + ]; +format_rate(connection_churn_rates, {TCCr, TCCo, TChCr, TChCo, TQD, TQCr, TQCo}, + {RCCr, RCCo, RChCr, RChCo, RQD, RQCr, RQCo}) -> + [ + {connection_created, TCCr}, + {connection_created_details, [{rate, RCCr}]}, + {connection_closed, TCCo}, + {connection_closed_details, [{rate, RCCo}]}, + {channel_created, TChCr}, + {channel_created_details, [{rate, RChCr}]}, + {channel_closed, TChCo}, + {channel_closed_details, [{rate, RChCo}]}, + {queue_declared, TQD}, + {queue_declared_details, [{rate, RQD}]}, + {queue_created, TQCr}, + {queue_created_details, [{rate, RQCr}]}, + {queue_deleted, TQCo}, + {queue_deleted_details, [{rate, RQCo}]} + ]. + +format_rate(connection_stats_coarse_conn_stats, {TR, TS, TRe}, {RR, RS, RRe}, + {SR, SS, SRe}, {STR, STS, STRe}, Length) -> + [ + {send_oct, TS}, + {send_oct_details, [{rate, RS}, + {samples, SS}] ++ average(SS, STS, Length)}, + {recv_oct, TR}, + {recv_oct_details, [{rate, RR}, + {samples, SR}] ++ average(SR, STR, Length)}, + {reductions, TRe}, + {reductions_details, [{rate, RRe}, + {samples, SRe}] ++ average(SRe, STRe, Length)} + ]; +format_rate(vhost_stats_coarse_conn_stats, {TR, TS}, {RR, RS}, {SR, SS}, + {STR, STS}, Length) -> + [ + {send_oct, TS}, + {send_oct_details, [{rate, RS}, + {samples, SS}] ++ average(SS, STS, Length)}, + {recv_oct, TR}, + {recv_oct_details, [{rate, RR}, + {samples, SR}] ++ average(SR, STR, Length)} + ]; +format_rate(Type, {TR, TW}, {RR, RW}, {SR, SW}, {STR, STW}, Length) + when Type =:= vhost_msg_rates; + Type =:= queue_msg_rates -> + [ + {disk_reads, TR}, + {disk_reads_details, [{rate, RR}, + {samples, SR}] ++ average(SR, STR, Length)}, + {disk_writes, TW}, + {disk_writes_details, [{rate, RW}, + {samples, SW}] ++ average(SW, STW, Length)} + ]; +format_rate(Type, {TP, TC, TRe, TDrop}, {RP, RC, RRe, RDrop}, + {SP, SC, SRe, SDrop}, {STP, STC, STRe, STDrop}, Length) + when Type =:= channel_stats_fine_stats; + Type =:= vhost_stats_fine_stats; + Type =:= channel_exchange_stats_fine_stats -> + [ + {publish, TP}, + {publish_details, [{rate, RP}, + {samples, SP}] ++ average(SP, STP, Length)}, + {confirm, TC}, + {confirm_details, [{rate, RC}, + {samples, SC}] ++ average(SC, STC, Length)}, + {return_unroutable, TRe}, + {return_unroutable_details, [{rate, RRe}, + {samples, SRe}] ++ average(SRe, STRe, Length)}, + {drop_unroutable, TDrop}, + {drop_unroutable_details, [{rate, RDrop}, + {samples, SDrop}] ++ average(SDrop, STDrop, Length)} + ]; +format_rate(Type, {TG, TGN, TD, TDN, TR, TA, TDG, TGE}, + {RG, RGN, RD, RDN, RR, RA, RDG, RGE}, + {SG, SGN, SD, SDN, SR, SA, SDG, SGE}, + {STG, STGN, STD, STDN, STR, STA, STDG, STGE}, + Length) + when Type =:= channel_queue_stats_deliver_stats; + Type =:= channel_stats_deliver_stats; + Type =:= vhost_stats_deliver_stats; + Type =:= queue_stats_deliver_stats -> + [ + {get, TG}, + {get_details, [{rate, RG}, + {samples, SG}] ++ average(SG, STG, Length)}, + {get_no_ack, TGN}, + {get_no_ack_details, [{rate, RGN}, + {samples, SGN}] ++ average(SGN, STGN, Length)}, + {deliver, TD}, + {deliver_details, [{rate, RD}, + {samples, SD}] ++ average(SD, STD, Length)}, + {deliver_no_ack, TDN}, + {deliver_no_ack_details, [{rate, RDN}, + {samples, SDN}] ++ average(SDN, STDN, Length)}, + {redeliver, TR}, + {redeliver_details, [{rate, RR}, + {samples, SR}] ++ average(SR, STR, Length)}, + {ack, TA}, + {ack_details, [{rate, RA}, + {samples, SA}] ++ average(SA, STA, Length)}, + {deliver_get, TDG}, + {deliver_get_details, [{rate, RDG}, + {samples, SDG}] ++ average(SDG, STDG, Length)}, + {get_empty, TGE}, + {get_empty_details, [{rate, RGE}, + {samples, SGE}] ++ average(SGE, STGE, Length)} + ]; +format_rate(Type, {TR}, {RR}, {SR}, {STR}, Length) + when Type =:= channel_process_stats; + Type =:= queue_process_stats -> + [ + {reductions, TR}, + {reductions_details, [{rate, RR}, + {samples, SR}] ++ average(SR, STR, Length)} + ]; +format_rate(exchange_stats_publish_out, {TP}, {RP}, {SP}, {STP}, Length) -> + [ + {publish_out, TP}, + {publish_out_details, [{rate, RP}, + {samples, SP}] ++ average(SP, STP, Length)} + ]; +format_rate(exchange_stats_publish_in, {TP}, {RP}, {SP}, {STP}, Length) -> + [ + {publish_in, TP}, + {publish_in_details, [{rate, RP}, + {samples, SP}] ++ average(SP, STP, Length)} + ]; +format_rate(Type, {TP}, {RP}, {SP}, {STP}, Length) + when Type =:= queue_stats_publish; + Type =:= queue_exchange_stats_publish -> + [ + {publish, TP}, + {publish_details, [{rate, RP}, + {samples, SP}] ++ average(SP, STP, Length)} + ]; +format_rate(Type, {TR, TU, TM}, {RR, RU, RM}, {SR, SU, SM}, {STR, STU, STM}, + Length) when Type =:= queue_msg_stats; + Type =:= vhost_msg_stats -> + [ + {messages_ready, TR}, + {messages_ready_details, [{rate, RR}, + {samples, SR}] ++ average(SR, STR, Length)}, + {messages_unacknowledged, TU}, + {messages_unacknowledged_details, [{rate, RU}, + {samples, SU}] ++ average(SU, STU, Length)}, + {messages, TM}, + {messages_details, [{rate, RM}, + {samples, SM}] ++ average(SM, STM, Length)} + ]; +format_rate(node_coarse_stats, {TF, TS, TM, TD, TP, TGC, TGCW, TCS}, + {RF, RS, RM, RD, RP, RGC, RGCW, RCS}, + {SF, SS, SM, SD, SP, SGC, SGCW, SCS}, + {STF, STS, STM, STD, STP, STGC, STGCW, STCS}, Length) -> + [ + {mem_used, TM}, + {mem_used_details, [{rate, RM}, + {samples, SM}] ++ average(SM, STM, Length)}, + {fd_used, TF}, + {fd_used_details, [{rate, RF}, + {samples, SF}] ++ average(SF, STF, Length)}, + {sockets_used, TS}, + {sockets_used_details, [{rate, RS}, + {samples, SS}] ++ average(SS, STS, Length)}, + {proc_used, TP}, + {proc_used_details, [{rate, RP}, + {samples, SP}] ++ average(SP, STP, Length)}, + {disk_free, TD}, + {disk_free_details, [{rate, RD}, + {samples, SD}] ++ average(SD, STD, Length)}, + {gc_num, TGC}, + {gc_num_details, [{rate, RGC}, + {samples, SGC}] ++ average(SGC, STGC, Length)}, + {gc_bytes_reclaimed, TGCW}, + {gc_bytes_reclaimed_details, [{rate, RGCW}, + {samples, SGCW}] ++ average(SGCW, STGCW, Length)}, + {context_switches, TCS}, + {context_switches_details, [{rate, RCS}, + {samples, SCS}] ++ average(SCS, STCS, Length)} + ]; +format_rate(node_persister_stats, + {TIR, TIB, TIA, TIWC, TIWB, TIWAT, TIS, TISAT, TISC, + TISEAT, TIRC, TMRTC, TMDTC, TMSRC, TMSWC, TQIJWC, TQIWC, TQIRC, + TIO, TIOAT}, + {RIR, RIB, _RIA, RIWC, RIWB, _RIWAT, RIS, _RISAT, RISC, + _RISEAT, RIRC, RMRTC, RMDTC, RMSRC, RMSWC, RQIJWC, RQIWC, RQIRC, + RIO, _RIOAT}, + {SIR, SIB, SIA, SIWC, SIWB, SIWAT, SIS, SISAT, SISC, + SISEAT, SIRC, SMRTC, SMDTC, SMSRC, SMSWC, SQIJWC, SQIWC, SQIRC, + SIO, SIOAT}, + {STIR, STIB, _STIA, STIWC, STIWB, _STIWAT, STIS, _STISAT, STISC, + _STISEAT, STIRC, STMRTC, STMDTC, STMSRC, STMSWC, STQIJWC, STQIWC, STQIRC, + STIO, _STIOAT}, Length) -> + %% Calculates average times for read/write/sync/seek from the + %% accumulated time and count + %% io_<op>_avg_time is the average operation time for the life of the node + %% io_<op>_avg_time_details/rate is the average operation time during the + %% last time unit calculated (thus similar to an instant rate) + + [ + {io_read_count, TIR}, + {io_read_count_details, [{rate, RIR}, + {samples, SIR}] ++ average(SIR, STIR, Length)}, + {io_read_bytes, TIB}, + {io_read_bytes_details, [{rate, RIB}, + {samples, SIB}] ++ average(SIB, STIB, Length)}, + {io_read_avg_time, avg_time(TIA, TIR)}, + {io_read_avg_time_details, [{samples, unit_samples(SIA, SIR)}] ++ + avg_time_details(avg_time(TIA, TIR))}, + {io_write_count, TIWC}, + {io_write_count_details, [{rate, RIWC}, + {samples, SIWC}] ++ average(SIWC, STIWC, Length)}, + {io_write_bytes, TIWB}, + {io_write_bytes_details, [{rate, RIWB}, + {samples, SIWB}] ++ average(SIWB, STIWB, Length)}, + {io_write_avg_time, avg_time(TIWAT, TIWC)}, + {io_write_avg_time_details, [{samples, unit_samples(SIWAT, SIWC)}] ++ + avg_time_details(avg_time(TIWAT, TIWC))}, + {io_sync_count, TIS}, + {io_sync_count_details, [{rate, RIS}, + {samples, SIS}] ++ average(SIS, STIS, Length)}, + {io_sync_avg_time, avg_time(TISAT, TIS)}, + {io_sync_avg_time_details, [{samples, unit_samples(SISAT, SIS)}] ++ + avg_time_details(avg_time(TISAT, TIS))}, + {io_seek_count, TISC}, + {io_seek_count_details, [{rate, RISC}, + {samples, SISC}] ++ average(SISC, STISC, Length)}, + {io_seek_avg_time, avg_time(TISEAT, TISC)}, + {io_seek_avg_time_details, [{samples, unit_samples(SISEAT, SISC)}] ++ + avg_time_details(avg_time(TISEAT, TISC))}, + {io_reopen_count, TIRC}, + {io_reopen_count_details, [{rate, RIRC}, + {samples, SIRC}] ++ average(SIRC, STIRC, Length)}, + {mnesia_ram_tx_count, TMRTC}, + {mnesia_ram_tx_count_details, [{rate, RMRTC}, + {samples, SMRTC}] ++ average(SMRTC, STMRTC, Length)}, + {mnesia_disk_tx_count, TMDTC}, + {mnesia_disk_tx_count_details, [{rate, RMDTC}, + {samples, SMDTC}] ++ average(SMDTC, STMDTC, Length)}, + {msg_store_read_count, TMSRC}, + {msg_store_read_count_details, [{rate, RMSRC}, + {samples, SMSRC}] ++ average(SMSRC, STMSRC, Length)}, + {msg_store_write_count, TMSWC}, + {msg_store_write_count_details, [{rate, RMSWC}, + {samples, SMSWC}] ++ average(SMSWC, STMSWC, Length)}, + {queue_index_journal_write_count, TQIJWC}, + {queue_index_journal_write_count_details, [{rate, RQIJWC}, + {samples, SQIJWC}] ++ average(SQIJWC, STQIJWC, Length)}, + {queue_index_write_count, TQIWC}, + {queue_index_write_count_details, [{rate, RQIWC}, + {samples, SQIWC}] ++ average(SQIWC, STQIWC, Length)}, + {queue_index_read_count, TQIRC}, + {queue_index_read_count_details, [{rate, RQIRC}, + {samples, SQIRC}] ++ average(SQIRC, STQIRC, Length)}, + {io_file_handle_open_attempt_count, TIO}, + {io_file_handle_open_attempt_count_details, [{rate, RIO}, + {samples, SIO}] ++ average(SIO, STIO, Length)}, + {io_file_handle_open_attempt_avg_time, avg_time(TIOAT, TIO)}, + {io_file_handle_open_attempt_avg_time_details, + [{samples, unit_samples(SIOAT, SIO)}] ++ avg_time_details(avg_time(TIOAT, TIO))} + ]; +format_rate(node_node_coarse_stats, {TS, TR}, {RS, RR}, {SS, SR}, {STS, STR}, Length) -> + [ + {send_bytes, TS}, + {send_bytes_details, [{rate, RS}, + {samples, SS}] ++ average(SS, STS, Length)}, + {recv_bytes, TR}, + {recv_bytes_details, [{rate, RR}, + {samples, SR}] ++ average(SR, STR, Length)} + ]; +format_rate(connection_churn_rates, {TCCr, TCCo, TChCr, TChCo, TQD, TQCr, TQCo}, + {RCCr, RCCo, RChCr, RChCo, RQD, RQCr, RQCo}, + {SCCr, SCCo, SChCr, SChCo, SQD, SQCr, SQCo}, + {STCCr, STCCo, STChCr, STChCo, STQD, STQCr, STQCo}, Length) -> + [ + {connection_created, TCCr}, + {connection_created_details, [{rate, RCCr}, + {samples, SCCr}] ++ average(SCCr, STCCr, Length)}, + {connection_closed, TCCo}, + {connection_closed_details, [{rate, RCCo}, + {samples, SCCo}] ++ average(SCCo, STCCo, Length)}, + {channel_created, TChCr}, + {channel_created_details, [{rate, RChCr}, + {samples, SChCr}] ++ average(SChCr, STChCr, Length)}, + {channel_closed, TChCo}, + {channel_closed_details, [{rate, RChCo}, + {samples, SChCo}] ++ average(SChCo, STChCo, Length)}, + {queue_declared, TQD}, + {queue_declared_details, [{rate, RQD}, + {samples, SQD}] ++ average(SQD, STQD, Length)}, + {queue_created, TQCr}, + {queue_created_details, [{rate, RQCr}, + {samples, SQCr}] ++ average(SQCr, STQCr, Length)}, + {queue_deleted, TQCo}, + {queue_deleted_details, [{rate, RQCo}, + {samples, SQCo}] ++ average(SQCo, STQCo, Length)} + ]. + +avg_time_details(Avg) -> + %% Rates don't make sense here, populate it with the average. + [{rate, Avg}, {avg_rate, Avg}, {avg, Avg}]. + +average(_Samples, _Total, Length) when Length =< 1-> + []; +average(Samples, Total, Length) -> + [{sample, S2}, {timestamp, T2}] = hd(Samples), + [{sample, S1}, {timestamp, T1}] = lists:last(Samples), + [{avg_rate, (S2 - S1) * 1000 / (T2 - T1)}, + {avg, Total / Length}]. + +rate_from_last_increment(Table, {TS, _} = Last, T, RangePoint) -> + case TS - RangePoint of % [0] + D when D >= 0 -> + case rate_from_last_increment(Last, T) of + unknown -> + empty(Table, 0.0); + Rate -> + Rate + end; + _ -> + empty(Table, 0.0) + end. + +%% [0] Only display the rate if it's live - i.e. ((the end of the +%% range) - interval) corresponds to the last data point we have +rate_from_last_increment(_Total, []) -> + unknown; +rate_from_last_increment(Total, [H | _T]) -> + rate_from_difference(Total, H). + +rate_from_difference({TS0, {A0, A1}}, {TS1, {B0, B1}}) -> + Interval = TS0 - TS1, + {rate(A0, B0, Interval), rate(A1, B1, Interval)}; +rate_from_difference({TS0, {A0, A1, A2}}, {TS1, {B0, B1, B2}}) -> + Interval = TS0 - TS1, + {rate(A0, B0, Interval), rate(A1, B1, Interval), rate(A2, B2, Interval)}; +rate_from_difference({TS0, {A0, A1, A2, A3}}, {TS1, {B0, B1, B2, B3}}) -> + Interval = TS0 - TS1, + {rate(A0, B0, Interval), rate(A1, B1, Interval), rate(A2, B2, Interval), rate(A3, B3, Interval)}; +rate_from_difference({TS0, {A0, A1, A2, A3, A4}}, {TS1, {B0, B1, B2, B3, B4}}) -> + Interval = TS0 - TS1, + {rate(A0, B0, Interval), rate(A1, B1, Interval), rate(A2, B2, Interval), rate(A3, B3, Interval), + rate(A4, B4, Interval)}; +rate_from_difference({TS0, {A0, A1, A2, A3, A4, A5}}, {TS1, {B0, B1, B2, B3, B4, B5}}) -> + Interval = TS0 - TS1, + {rate(A0, B0, Interval), rate(A1, B1, Interval), rate(A2, B2, Interval), rate(A3, B3, Interval), + rate(A4, B4, Interval), rate(A5, B5, Interval)}; +rate_from_difference({TS0, {A0, A1, A2, A3, A4, A5, A6}}, + {TS1, {B0, B1, B2, B3, B4, B5, B6}}) -> + Interval = TS0 - TS1, + {rate(A0, B0, Interval), rate(A1, B1, Interval), rate(A2, B2, Interval), + rate(A3, B3, Interval), rate(A4, B4, Interval), rate(A5, B5, Interval), + rate(A6, B6, Interval)}; +rate_from_difference({TS0, {A0, A1, A2, A3, A4, A5, A6, A7}}, + {TS1, {B0, B1, B2, B3, B4, B5, B6, B7}}) -> + Interval = TS0 - TS1, + {rate(A0, B0, Interval), rate(A1, B1, Interval), rate(A2, B2, Interval), + rate(A3, B3, Interval), rate(A4, B4, Interval), rate(A5, B5, Interval), + rate(A6, B6, Interval), rate(A7, B7, Interval)}; +rate_from_difference({TS0, {A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, + A14, A15, A16, A17, A18, A19}}, + {TS1, {B0, B1, B2, B3, B4, B5, B6, B7, B8, B9, B10, B11, B12, B13, + B14, B15, B16, B17, B18, B19}}) -> + Interval = TS0 - TS1, + {rate(A0, B0, Interval), rate(A1, B1, Interval), rate(A2, B2, Interval), + rate(A3, B3, Interval), rate(A4, B4, Interval), rate(A5, B5, Interval), + rate(A6, B6, Interval), rate(A7, B7, Interval), rate(A8, B8, Interval), + rate(A9, B9, Interval), rate(A10, B10, Interval), rate(A11, B11, Interval), + rate(A12, B12, Interval), rate(A13, B13, Interval), rate(A14, B14, Interval), + rate(A15, B15, Interval), rate(A16, B16, Interval), rate(A17, B17, Interval), + rate(A18, B18, Interval), rate(A19, B19, Interval)}; +rate_from_difference({TS0, {A0}}, {TS1, {B0}}) -> + Interval = TS0 - TS1, + {rate(A0, B0, Interval)}. + +rate(V1, V2, Interval) when is_number(V1), is_number(V2) -> + (V1 - V2) * 1000 / Interval; +rate(_, _, _) -> + 0. + +append_sample(S, TS, List) -> + [[{sample, S}, {timestamp, TS}] | List]. + +%%---------------------------------------------------------------------------- +%% Match specs to select from the ETS tables +%%---------------------------------------------------------------------------- + +avg_time(_Total, Count) when Count == 0; + Count == 0.0 -> + 0.0; +avg_time(Total, Count) -> + (Total / Count) / ?MICRO_TO_MILLI. + +unit_samples(Total, Count) -> + lists:zipwith(fun(T, C) -> + TS = proplists:get_value(timestamp, T), + Sample = avg_time(proplists:get_value(sample, T), + proplists:get_value(sample, C)), + [{sample, Sample}, {timestamp, TS}] + end, Total, Count). + +empty(Table, Def) -> + rabbit_mgmt_data:empty(Table, Def). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_sup.erl b/deps/rabbitmq_management/src/rabbit_mgmt_sup.erl new file mode 100644 index 0000000000..5f34f0d160 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_sup.erl @@ -0,0 +1,42 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2011-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_sup). + +-behaviour(supervisor). + +-export([init/1]). +-export([start_link/0]). +-export([setup_wm_logging/0]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_metrics.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit_core_metrics.hrl"). + +init([]) -> + DB = {rabbit_mgmt_db, {rabbit_mgmt_db, start_link, []}, + permanent, ?WORKER_WAIT, worker, [rabbit_mgmt_db]}, + WP = {management_worker_pool_sup, {worker_pool_sup, start_link, [3, management_worker_pool]}, + permanent, ?SUPERVISOR_WAIT, supervisor, [management_worker_pool_sup]}, + DBC = {rabbit_mgmt_db_cache_sup, {rabbit_mgmt_db_cache_sup, start_link, []}, + permanent, ?SUPERVISOR_WAIT, supervisor, [rabbit_mgmt_db_cache_sup]}, + {ok, {{one_for_one, 100, 1}, [DB, WP, DBC]}}. + +start_link() -> + Res = supervisor:start_link({local, ?MODULE}, ?MODULE, []), + setup_wm_logging(), + Res. + +%% While the project has switched to Cowboy for HTTP handling, we still use +%% the logger from Webmachine; at least until RabbitMQ switches to Lager or +%% similar. +setup_wm_logging() -> + {ok, LogDir} = application:get_env(rabbitmq_management, http_log_dir), + case LogDir of + none -> ok; + _ -> webmachine_log:add_handler(webmachine_log_handler, [LogDir]) + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_sup_sup.erl b/deps/rabbitmq_management/src/rabbit_mgmt_sup_sup.erl new file mode 100644 index 0000000000..e8dd164869 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_sup_sup.erl @@ -0,0 +1,28 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_sup_sup). + +-behaviour(supervisor2). + +-export([init/1]). +-export([start_link/0, start_child/0]). + +-include_lib("rabbit_common/include/rabbit.hrl"). + +start_child() -> + supervisor2:start_child(?MODULE, sup()). + +sup() -> + {rabbit_mgmt_sup, {rabbit_mgmt_sup, start_link, []}, + temporary, ?SUPERVISOR_WAIT, supervisor, [rabbit_mgmt_sup]}. + +init([]) -> + {ok, {{one_for_one, 0, 1}, [sup()]}}. + +start_link() -> + supervisor2:start_link({local, ?MODULE}, ?MODULE, []). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_util.erl b/deps/rabbitmq_management/src/rabbit_mgmt_util.erl new file mode 100644 index 0000000000..93f167e480 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_util.erl @@ -0,0 +1,1220 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_util). + +%% TODO sort all this out; maybe there's scope for rabbit_mgmt_request? + +-export([is_authorized/2, is_authorized_admin/2, is_authorized_admin/4, + is_authorized_admin/3, vhost/1, vhost_from_headers/1]). +-export([is_authorized_vhost/2, is_authorized_user/3, + is_authorized_user/4, + is_authorized_monitor/2, is_authorized_policies/2, + is_authorized_vhost_visible/2, + is_authorized_global_parameters/2]). + +-export([bad_request/3, bad_request_exception/4, internal_server_error/4, + id/2, parse_bool/1, parse_int/1]). +-export([with_decode/4, not_found/3]). +-export([with_channel/4, with_channel/5]). +-export([props_to_method/2, props_to_method/4]). +-export([all_or_one_vhost/2, reply/3, responder_map/1, + filter_vhost/3]). +-export([filter_conn_ch_list/3, filter_user/2, list_login_vhosts/2, + list_login_vhosts_names/2]). +-export([filter_tracked_conn_list/3]). +-export([with_decode/5, decode/1, decode/2, set_resp_header/3, + args/1, read_complete_body/1]). +-export([reply_list/3, reply_list/5, reply_list/4, + sort_list/2, destination_type/1, reply_list_or_paginate/3 + ]). +-export([post_respond/1, columns/1, is_monitor/1]). +-export([list_visible_vhosts/1, list_visible_vhosts_names/1, + b64decode_or_throw/1, no_range/0, range/1, + range_ceil/1, floor/2, ceil/1, ceil/2]). +-export([pagination_params/1, + maybe_filter_by_keyword/4, + get_value_param/2, + augment_resources/6 + ]). +-export([direct_request/6]). +-export([qs_val/2]). +-export([get_path_prefix/0]). +-export([catch_no_such_user_or_vhost/2]). + +-export([disable_stats/1, enable_queue_totals/1]). + +-import(rabbit_misc, [pget/2]). + +-include("rabbit_mgmt.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). + +-define(FRAMING, rabbit_framing_amqp_0_9_1). +-define(DEFAULT_PAGE_SIZE, 100). +-define(MAX_PAGE_SIZE, 500). + +-define(MAX_RANGE, 500). + +-record(pagination, {page = undefined, page_size = undefined, + name = undefined, use_regex = undefined}). + +-record(aug_ctx, {req_data :: cowboy_req:req(), + pagination :: #pagination{}, + sort = [] :: [atom()], + columns = [] :: [atom()], + data :: term()}). + +%%-------------------------------------------------------------------- + +is_authorized(ReqData, Context) -> + is_authorized(ReqData, Context, '', fun(_) -> true end). + +is_authorized_admin(ReqData, Context) -> + is_authorized(ReqData, Context, + <<"Not administrator user">>, + fun(#user{tags = Tags}) -> is_admin(Tags) end). + +is_authorized_admin(ReqData, Context, Token) -> + is_authorized(ReqData, Context, + rabbit_data_coercion:to_binary( + application:get_env(rabbitmq_management, uaa_client_id, "")), + Token, <<"Not administrator user">>, + fun(#user{tags = Tags}) -> is_admin(Tags) end). + +is_authorized_admin(ReqData, Context, Username, Password) -> + case is_basic_auth_disabled() of + true -> + Msg = "HTTP access denied: basic auth disabled", + rabbit_log:warning(Msg), + not_authorised(Msg, ReqData, Context); + false -> + is_authorized(ReqData, Context, Username, Password, + <<"Not administrator user">>, + fun(#user{tags = Tags}) -> is_admin(Tags) end) + end. + +is_authorized_monitor(ReqData, Context) -> + is_authorized(ReqData, Context, + <<"Not monitor user">>, + fun(#user{tags = Tags}) -> is_monitor(Tags) end). + +is_authorized_vhost(ReqData, Context) -> + is_authorized(ReqData, Context, + <<"User not authorised to access virtual host">>, + fun(#user{tags = Tags} = User) -> + is_admin(Tags) orelse user_matches_vhost(ReqData, User) + end). + +is_authorized_vhost_visible(ReqData, Context) -> + is_authorized(ReqData, Context, + <<"User not authorised to access virtual host">>, + fun(#user{tags = Tags} = User) -> + is_admin(Tags) orelse user_matches_vhost_visible(ReqData, User) + end). + +disable_stats(ReqData) -> + MgmtOnly = case qs_val(<<"disable_stats">>, ReqData) of + <<"true">> -> true; + _ -> false + end, + MgmtOnly orelse get_bool_env(rabbitmq_management, disable_management_stats, false) + orelse get_bool_env(rabbitmq_management_agent, disable_metrics_collector, false). + +enable_queue_totals(ReqData) -> + EnableTotals = case qs_val(<<"enable_queue_totals">>, ReqData) of + <<"true">> -> true; + _ -> false + end, + EnableTotals orelse get_bool_env(rabbitmq_management, enable_queue_totals, false). + +get_bool_env(Application, Par, Default) -> + case application:get_env(Application, Par, Default) of + true -> true; + false -> false; + Other -> + rabbit_log:warning("Invalid configuration for application ~p: ~p set to ~p", + [Application, Par, Other]), + Default + end. + +user_matches_vhost(ReqData, User) -> + case vhost(ReqData) of + not_found -> true; + none -> true; + V -> + AuthzData = get_authz_data(ReqData), + lists:member(V, list_login_vhosts_names(User, AuthzData)) + end. + +user_matches_vhost_visible(ReqData, User) -> + case vhost(ReqData) of + not_found -> true; + none -> true; + V -> + AuthzData = get_authz_data(ReqData), + lists:member(V, list_visible_vhosts_names(User, AuthzData)) + end. + +get_authz_data(ReqData) -> + {PeerAddress, _PeerPort} = cowboy_req:peer(ReqData), + {ip, PeerAddress}. + +%% Used for connections / channels. A normal user can only see / delete +%% their own stuff. Monitors can see other users' and delete their +%% own. Admins can do it all. +is_authorized_user(ReqData, Context, Item) -> + is_authorized(ReqData, Context, + <<"User not authorised to access object">>, + fun(#user{username = Username, tags = Tags}) -> + case cowboy_req:method(ReqData) of + <<"DELETE">> -> is_admin(Tags); + _ -> is_monitor(Tags) + end orelse Username == pget(user, Item) + end). + +%% For policies / parameters. Like is_authorized_vhost but you have to +%% be a policymaker. +is_authorized_policies(ReqData, Context) -> + is_authorized(ReqData, Context, + <<"User not authorised to access object">>, + fun(User = #user{tags = Tags}) -> + is_admin(Tags) orelse + (is_policymaker(Tags) andalso + user_matches_vhost(ReqData, User)) + end). + +%% For global parameters. Must be policymaker. +is_authorized_global_parameters(ReqData, Context) -> + is_authorized(ReqData, Context, + <<"User not authorised to access object">>, + fun(#user{tags = Tags}) -> + is_policymaker(Tags) + end). + +is_basic_auth_disabled() -> + get_bool_env(rabbitmq_management, disable_basic_auth, false). + +is_authorized(ReqData, Context, ErrorMsg, Fun) -> + case cowboy_req:method(ReqData) of + <<"OPTIONS">> -> {true, ReqData, Context}; + _ -> is_authorized1(ReqData, Context, ErrorMsg, Fun) + end. + +is_authorized1(ReqData, Context, ErrorMsg, Fun) -> + case cowboy_req:parse_header(<<"authorization">>, ReqData) of + {basic, Username, Password} -> + case is_basic_auth_disabled() of + true -> + Msg = "HTTP access denied: basic auth disabled", + rabbit_log:warning(Msg), + not_authorised(Msg, ReqData, Context); + false -> + is_authorized(ReqData, Context, + Username, Password, + ErrorMsg, Fun) + end; + {bearer, Token} -> + Username = rabbit_data_coercion:to_binary( + application:get_env(rabbitmq_management, uaa_client_id, "")), + is_authorized(ReqData, Context, Username, Token, ErrorMsg, Fun); + _ -> + case is_basic_auth_disabled() of + true -> + Msg = "HTTP access denied: basic auth disabled", + rabbit_log:warning(Msg), + not_authorised(Msg, ReqData, Context); + false -> + {{false, ?AUTH_REALM}, ReqData, Context} + end + end. + +is_authorized_user(ReqData, Context, Username, Password) -> + Msg = <<"User not authorized">>, + Fun = fun(_) -> true end, + is_authorized(ReqData, Context, Username, Password, Msg, Fun). + +is_authorized(ReqData, Context, Username, Password, ErrorMsg, Fun) -> + ErrFun = fun (Msg) -> + rabbit_log:warning("HTTP access denied: user '~s' - ~s", + [Username, Msg]), + not_authorised(Msg, ReqData, Context) + end, + AuthProps = [{password, Password}] ++ case vhost(ReqData) of + VHost when is_binary(VHost) -> [{vhost, VHost}]; + _ -> [] + end, + {IP, _} = cowboy_req:peer(ReqData), + RemoteAddress = list_to_binary(inet:ntoa(IP)), + case rabbit_access_control:check_user_login(Username, AuthProps) of + {ok, User = #user{tags = Tags}} -> + case rabbit_access_control:check_user_loopback(Username, IP) of + ok -> + case is_mgmt_user(Tags) of + true -> + case Fun(User) of + true -> + rabbit_core_metrics:auth_attempt_succeeded(RemoteAddress, + Username, http), + {true, ReqData, + Context#context{user = User, + password = Password}}; + false -> + rabbit_core_metrics:auth_attempt_failed(RemoteAddress, + Username, http), + ErrFun(ErrorMsg) + end; + false -> + rabbit_core_metrics:auth_attempt_failed(RemoteAddress, Username, http), + ErrFun(<<"Not management user">>) + end; + not_allowed -> + rabbit_core_metrics:auth_attempt_failed(RemoteAddress, Username, http), + ErrFun(<<"User can only log in via localhost">>) + end; + {refused, _Username, Msg, Args} -> + rabbit_core_metrics:auth_attempt_failed(RemoteAddress, Username, http), + rabbit_log:warning("HTTP access denied: ~s", + [rabbit_misc:format(Msg, Args)]), + not_authorised(<<"Login failed">>, ReqData, Context) + end. + +vhost_from_headers(ReqData) -> + case cowboy_req:header(<<"x-vhost">>, ReqData) of + undefined -> none; + %% blank x-vhost means "All hosts" is selected in the UI + <<>> -> none; + VHost -> VHost + end. + +vhost(ReqData) -> + case id(vhost, ReqData) of + none -> vhost_from_headers(ReqData); + VHost -> case rabbit_vhost:exists(VHost) of + true -> VHost; + false -> not_found + end + end. + +destination_type(ReqData) -> + case id(dtype, ReqData) of + <<"e">> -> exchange; + <<"q">> -> queue + end. + +%% Provides a map of content type-to-responder that are supported by +%% reply/3. The map can be used in the content_types_provided/2 callback +%% used by cowboy_rest. Responder functions must be +%% exported from the caller module and must use reply/3 +%% under the hood. +responder_map(FunctionName) -> + [ + {<<"application/json">>, FunctionName}, + {<<"application/bert">>, FunctionName} + ]. + +reply({stop, _, _} = Reply, _ReqData, _Context) -> + Reply; +reply(Facts, ReqData, Context) -> + reply0(extract_columns(Facts, ReqData), ReqData, Context). + +reply0(Facts, ReqData, Context) -> + ReqData1 = set_resp_header(<<"cache-control">>, "no-cache", ReqData), + try + case maps:get(media_type, ReqData1, undefined) of + {<<"application">>, <<"bert">>, _} -> + {term_to_binary(Facts), ReqData1, Context}; + _ -> + {rabbit_json:encode(rabbit_mgmt_format:format_nulls(Facts)), + ReqData1, Context} + end + catch exit:{json_encode, E} -> + Error = iolist_to_binary( + io_lib:format("JSON encode error: ~p", [E])), + Reason = iolist_to_binary( + io_lib:format("While encoding: ~n~p", [Facts])), + internal_server_error(Error, Reason, ReqData1, Context) + end. + +reply_list(Facts, ReqData, Context) -> + reply_list(Facts, ["vhost", "name"], ReqData, Context, undefined). + +reply_list(Facts, DefaultSorts, ReqData, Context) -> + reply_list(Facts, DefaultSorts, ReqData, Context, undefined). + +get_value_param(Name, ReqData) -> + case qs_val(Name, ReqData) of + undefined -> undefined; + Bin -> binary_to_list(Bin) + end. + +reply_list(Facts, DefaultSorts, ReqData, Context, Pagination) -> + SortList = + sort_list_and_paginate( + extract_columns_list(Facts, ReqData), + DefaultSorts, + get_value_param(<<"sort">>, ReqData), + get_sort_reverse(ReqData), Pagination), + + reply(SortList, ReqData, Context). + +-spec get_sort_reverse(cowboy_req:req()) -> atom(). +get_sort_reverse(ReqData) -> + case get_value_param(<<"sort_reverse">>, ReqData) of + undefined -> false; + V -> list_to_atom(V) + end. + + +-spec is_pagination_requested(#pagination{} | undefined) -> boolean(). +is_pagination_requested(undefined) -> + false; +is_pagination_requested(#pagination{}) -> + true. + + +with_valid_pagination(ReqData, Context, Fun) -> + try + Pagination = pagination_params(ReqData), + Fun(Pagination) + catch error:badarg -> + Reason = iolist_to_binary( + io_lib:format("Pagination parameters are invalid", [])), + invalid_pagination(bad_request, Reason, ReqData, Context); + {error, ErrorType, S} -> + Reason = iolist_to_binary(S), + invalid_pagination(ErrorType, Reason, ReqData, Context) + end. + +reply_list_or_paginate(Facts, ReqData, Context) -> + with_valid_pagination( + ReqData, Context, + fun(Pagination) -> + reply_list(Facts, ["vhost", "name"], ReqData, Context, Pagination) + end). + +merge_sorts(DefaultSorts, Extra) -> + case Extra of + undefined -> DefaultSorts; + Extra -> [Extra | DefaultSorts] + end. + +%% Resource augmentation. Works out the most optimal configuration of the operations: +%% sort, page, augment and executes it returning the result. + +column_strategy(all, _) -> extended; +column_strategy(Cols, BasicColumns) -> + case Cols -- BasicColumns of + [] -> basic; + _ -> extended + end. + +% columns are [[binary()]] - this takes the first item +columns_as_strings(all) -> all; +columns_as_strings(Columns0) -> + [rabbit_data_coercion:to_list(C) || [C | _] <- Columns0]. + +augment_resources(Resources, DefaultSort, BasicColumns, ReqData, Context, + AugmentFun) -> + with_valid_pagination(ReqData, Context, + fun (Pagination) -> + augment_resources0(Resources, DefaultSort, + BasicColumns, Pagination, + ReqData, AugmentFun) + end). + +augment_resources0(Resources, DefaultSort, BasicColumns, Pagination, ReqData, + AugmentFun) -> + SortFun = fun (AugCtx) -> sort(DefaultSort, AugCtx) end, + AugFun = fun (AugCtx) -> augment(AugmentFun, AugCtx) end, + PageFun = fun page/1, + Pagination = pagination_params(ReqData), + Sort = def(get_value_param(<<"sort">>, ReqData), DefaultSort), + Columns = def(columns(ReqData), all), + ColumnsAsStrings = columns_as_strings(Columns), + Pipeline = + case {Pagination =/= undefined, + column_strategy(Sort, BasicColumns), + column_strategy(ColumnsAsStrings, BasicColumns)} of + {false, basic, basic} -> % no pagination, no extended fields + [SortFun]; + {false, _, _} -> + % no pagination, extended columns means we need to augment all - SLOW + [AugFun, SortFun]; + {true, basic, basic} -> + [SortFun, PageFun]; + {true, extended, _} -> + % pagination with extended sort columns - SLOW + [AugFun, SortFun, PageFun]; + {true, basic, extended} -> + % pagination with extended columns and sorting on basic + % here we can reduce the augmentation set before + % augmenting + [SortFun, PageFun, AugFun] + end, + #aug_ctx{data = {_, Reply}} = run_augmentation( + #aug_ctx{req_data = ReqData, + pagination = Pagination, + sort = Sort, + columns = Columns, + data = {loaded, Resources}}, + Pipeline), + rabbit_mgmt_format:strip_pids(Reply). + +run_augmentation(C, []) -> C; +run_augmentation(C, [Next | Rem]) -> + C2 = Next(C), + run_augmentation(C2, Rem). + +sort(DefaultSort, Ctx = #aug_ctx{data = Data0, + req_data = ReqData, + sort = Sort}) -> + Data1 = get_data(Data0), + Data = sort_list(Data1, DefaultSort, Sort, + get_sort_reverse(ReqData)), + Ctx#aug_ctx{data = update_data(Data0, {sorted, Data})}. + +page(Ctx = #aug_ctx{data = Data0, + pagination = Pagination}) -> + Data = filter_and_paginate(get_data(Data0), Pagination), + Ctx#aug_ctx{data = update_data(Data0, {paged, Data})}. + +update_data({paged, Old}, New) -> + {paged, rabbit_misc:pset(items, get_data(New), Old)}; +update_data(_, New) -> + New. + +augment(AugmentFun, Ctx = #aug_ctx{data = Data0, req_data = ReqData}) -> + Data1 = get_data(Data0), + Data = AugmentFun(Data1, ReqData), + Ctx#aug_ctx{data = update_data(Data0, {augmented, Data})}. + +get_data({paged, Data}) -> + rabbit_misc:pget(items, Data); +get_data({_, Data}) -> + Data. + +get_path_prefix() -> + EnvPrefix = rabbit_misc:get_env(rabbitmq_management, path_prefix, ""), + fixup_prefix(EnvPrefix). + +fixup_prefix("") -> + ""; +fixup_prefix([Char|_Rest]=EnvPrefix) when is_list(EnvPrefix), Char =:= $/ -> + EnvPrefix; +fixup_prefix(EnvPrefix) when is_list(EnvPrefix) -> + "/" ++ EnvPrefix; +fixup_prefix(EnvPrefix) when is_binary(EnvPrefix) -> + fixup_prefix(rabbit_data_coercion:to_list(EnvPrefix)). + +%% XXX sort_list_and_paginate/2 is a more proper name for this function, keeping it +%% with this name for backwards compatibility +-spec sort_list([Fact], [string()]) -> [Fact] when + Fact :: [{atom(), term()}]. +sort_list(Facts, Sorts) -> sort_list_and_paginate(Facts, Sorts, undefined, false, + undefined). + +-spec sort_list([Fact], [SortColumn], [SortColumn] | undefined, boolean()) -> [Fact] when + Fact :: [{atom(), term()}], + SortColumn :: string(). +sort_list(Facts, _, [], _) -> + %% Do not sort when we are explicitly requested to sort with an + %% empty sort columns list. Note that this clause won't match when + %% 'sort' parameter is not provided in a HTTP request at all. + Facts; +sort_list(Facts, DefaultSorts, Sort, Reverse) -> + SortList = merge_sorts(DefaultSorts, Sort), + %% lists:sort/2 is much more expensive than lists:sort/1 + Sorted = [V || {_K, V} <- lists:sort( + [{sort_key(F, SortList), F} || F <- Facts])], + maybe_reverse(Sorted, Reverse). + +sort_list_and_paginate(Facts, DefaultSorts, Sort, Reverse, Pagination) -> + filter_and_paginate(sort_list(Facts, DefaultSorts, Sort, Reverse), Pagination). + +filter_and_paginate(Sorted, Pagination) -> + ContextList = maybe_filter_context(Sorted, Pagination), + range_filter(ContextList, Pagination, Sorted). + +%% +%% Filtering functions +%% +maybe_filter_context(List, #pagination{name = Name, use_regex = UseRegex}) when + is_list(Name) -> + lists:filter(fun(ListF) -> + maybe_filter_by_keyword(name, Name, ListF, UseRegex) + end, + List); +%% Here it is backward with the other API(s), that don't filter the data +maybe_filter_context(List, _) -> + List. + + +match_value({_, Value}, ValueTag, UseRegex) when UseRegex =:= "true" -> + case re:run(Value, ValueTag, [caseless]) of + {match, _} -> true; + nomatch -> false + end; +match_value({_, Value}, ValueTag, _) -> + Pos = string:str(string:to_lower(binary_to_list(Value)), + string:to_lower(ValueTag)), + case Pos of + Pos when Pos > 0 -> true; + _ -> false + end. + +maybe_filter_by_keyword(KeyTag, ValueTag, List, UseRegex) when + is_list(ValueTag), length(ValueTag) > 0 -> + match_value(lists:keyfind(KeyTag, 1, List), ValueTag, UseRegex); +maybe_filter_by_keyword(_, _, _, _) -> + true. + +check_request_param(V, ReqData) -> + case qs_val(V, ReqData) of + undefined -> undefined; + Str -> list_to_integer(binary_to_list(Str)) + end. + +%% Validates and returns pagination parameters: +%% Page is assumed to be > 0, PageSize > 0 PageSize <= ?MAX_PAGE_SIZE +pagination_params(ReqData) -> + PageNum = check_request_param(<<"page">>, ReqData), + PageSize = check_request_param(<<"page_size">>, ReqData), + Name = get_value_param(<<"name">>, ReqData), + UseRegex = get_value_param(<<"use_regex">>, ReqData), + case {PageNum, PageSize} of + {undefined, _} -> + undefined; + {PageNum, undefined} when is_integer(PageNum) andalso PageNum > 0 -> + #pagination{page = PageNum, page_size = ?DEFAULT_PAGE_SIZE, + name = Name, use_regex = UseRegex}; + {PageNum, PageSize} when is_integer(PageNum) + andalso is_integer(PageSize) + andalso (PageNum > 0) + andalso (PageSize > 0) + andalso (PageSize =< ?MAX_PAGE_SIZE) -> + #pagination{page = PageNum, page_size = PageSize, + name = Name, use_regex = UseRegex}; + _ -> throw({error, invalid_pagination_parameters, + io_lib:format("Invalid pagination parameters: page number ~p, page size ~p", + [PageNum, PageSize])}) + end. + +-spec maybe_reverse([any()], string() | true | false) -> [any()]. +maybe_reverse([], _) -> + []; +maybe_reverse(RangeList, true) when is_list(RangeList) -> + lists:reverse(RangeList); +maybe_reverse(RangeList, false) -> + RangeList. + +%% for backwards compatibility, does not filter the list +range_filter(List, undefined, _) + -> List; + +range_filter(List, RP = #pagination{page = PageNum, page_size = PageSize}, + TotalElements) -> + Offset = (PageNum - 1) * PageSize + 1, + try + range_response(sublist(List, Offset, PageSize), RP, List, + TotalElements) + catch + error:function_clause -> + Reason = io_lib:format( + "Page out of range, page: ~p page size: ~p, len: ~p", + [PageNum, PageSize, length(List)]), + throw({error, page_out_of_range, Reason}) + end. + +%% Injects pagination information into +range_response([], #pagination{page = PageNum, page_size = PageSize}, + TotalFiltered, TotalElements) -> + TotalPages = trunc((length(TotalFiltered) + PageSize - 1) / PageSize), + [{total_count, length(TotalElements)}, + {item_count, 0}, + {filtered_count, length(TotalFiltered)}, + {page, PageNum}, + {page_size, PageSize}, + {page_count, TotalPages}, + {items, []} + ]; +range_response(List, #pagination{page = PageNum, page_size = PageSize}, + TotalFiltered, TotalElements) -> + TotalPages = trunc((length(TotalFiltered) + PageSize - 1) / PageSize), + [{total_count, length(TotalElements)}, + {item_count, length(List)}, + {filtered_count, length(TotalFiltered)}, + {page, PageNum}, + {page_size, PageSize}, + {page_count, TotalPages}, + {items, List} + ]. + +sort_key(_Item, []) -> + []; +sort_key(Item, [Sort | Sorts]) -> + [get_dotted_value(Sort, Item) | sort_key(Item, Sorts)]. + +get_dotted_value(Key, Item) -> + Keys = string:tokens(Key, "."), + get_dotted_value0(Keys, Item). + +get_dotted_value0([Key], Item) -> + %% Put "nothing" before everything else, in number terms it usually + %% means 0. + pget_bin(list_to_binary(Key), Item, 0); +get_dotted_value0([Key | Keys], Item) -> + get_dotted_value0(Keys, pget_bin(list_to_binary(Key), Item, [])). + +pget_bin(Key, Map, Default) when is_map(Map) -> + maps:get(Key, Map, Default); +pget_bin(Key, List, Default) when is_list(List) -> + case lists:partition(fun ({K, _V}) -> a2b(K) =:= Key end, List) of + {[{_K, V}], _} -> V; + {[], _} -> Default + end. +maybe_pagination(Item, false, ReqData) -> + extract_column_items(Item, columns(ReqData)); +maybe_pagination([{items, Item} | T], true, ReqData) -> + [{items, extract_column_items(Item, columns(ReqData))} | + maybe_pagination(T, true, ReqData)]; +maybe_pagination([H | T], true, ReqData) -> + [H | maybe_pagination(T,true, ReqData)]; +maybe_pagination(Item, true, ReqData) -> + [maybe_pagination(X, true, ReqData) || X <- Item]. + +extract_columns(Item, ReqData) -> + maybe_pagination(Item, is_pagination_requested(pagination_params(ReqData)), + ReqData). + +extract_columns_list(Items, ReqData) -> + Cols = columns(ReqData), + [extract_column_items(Item, Cols) || Item <- Items]. + +columns(ReqData) -> + case qs_val(<<"columns">>, ReqData) of + undefined -> all; + Bin -> [[list_to_binary(T) || T <- string:tokens(C, ".")] + || C <- string:tokens(binary_to_list(Bin), ",")] + end. + +extract_column_items(Item, all) -> + Item; +extract_column_items(Item = [T | _], Cols) when is_tuple(T) -> + [{K, extract_column_items(V, descend_columns(a2b(K), Cols))} || + {K, V} <- Item, want_column(a2b(K), Cols)]; +extract_column_items(L, Cols) when is_list(L) -> + [extract_column_items(I, Cols) || I <- L]; +extract_column_items(O, _Cols) -> + O. + +% want_column(_Col, all) -> true; +want_column(Col, Cols) -> lists:any(fun([C|_]) -> C == Col end, Cols). + +descend_columns(_K, []) -> []; +descend_columns( K, [[K] | _Rest]) -> all; +descend_columns( K, [[K | K2] | Rest]) -> [K2 | descend_columns(K, Rest)]; +descend_columns( K, [[_K2 | _ ] | Rest]) -> descend_columns(K, Rest). + +a2b(A) when is_atom(A) -> list_to_binary(atom_to_list(A)); +a2b(B) -> B. + +bad_request(Reason, ReqData, Context) -> + halt_response(400, bad_request, Reason, ReqData, Context). + +not_authorised(Reason, ReqData, Context) -> + halt_response(401, not_authorised, Reason, ReqData, Context). + +not_found(Reason, ReqData, Context) -> + halt_response(404, not_found, Reason, ReqData, Context). + +internal_server_error(Error, Reason, ReqData, Context) -> + rabbit_log:error("~s~n~s", [Error, Reason]), + halt_response(500, Error, Reason, ReqData, Context). + +invalid_pagination(Type,Reason, ReqData, Context) -> + halt_response(400, Type, Reason, ReqData, Context). + +halt_response(Code, Type, Reason, ReqData, Context) -> + ReasonFormatted = format_reason(Reason), + Json = #{<<"error">> => Type, + <<"reason">> => ReasonFormatted}, + ReqData1 = cowboy_req:reply(Code, + #{<<"content-type">> => <<"application/json">>}, + rabbit_json:encode(Json), ReqData), + {stop, ReqData1, Context}. + +format_reason(Tuple) when is_tuple(Tuple) -> + rabbit_mgmt_format:tuple(Tuple); +format_reason(Binary) when is_binary(Binary) -> + Binary; +format_reason(Other) -> + case is_string(Other) of + true -> rabbit_mgmt_format:print("~ts", [Other]); + false -> rabbit_mgmt_format:print("~p", [Other]) + end. + +is_string(List) when is_list(List) -> + lists:all( + fun(El) -> is_integer(El) andalso El > 0 andalso El < 16#10ffff end, + List); +is_string(_) -> false. + +id(Key, ReqData) when Key =:= exchange; + Key =:= source; + Key =:= destination -> + case id0(Key, ReqData) of + <<"amq.default">> -> <<"">>; + Name -> Name + end; +id(Key, ReqData) -> + id0(Key, ReqData). + +id0(Key, ReqData) -> + case cowboy_req:binding(Key, ReqData) of + undefined -> none; + Id -> Id + end. + +read_complete_body(Req) -> + read_complete_body(Req, <<"">>). +read_complete_body(Req0, Acc) -> + case cowboy_req:read_body(Req0) of + {ok, Data, Req} -> {ok, <<Acc/binary, Data/binary>>, Req}; + {more, Data, Req} -> read_complete_body(Req, <<Acc/binary, Data/binary>>) + end. + +with_decode(Keys, ReqData, Context, Fun) -> + {ok, Body, ReqData1} = read_complete_body(ReqData), + with_decode(Keys, Body, ReqData1, Context, Fun). + +with_decode(Keys, Body, ReqData, Context, Fun) -> + case decode(Keys, Body) of + {error, Reason} -> bad_request(Reason, ReqData, Context); + {ok, Values, JSON} -> try + Fun(Values, JSON, ReqData) + catch {error, Error} -> + bad_request(Error, ReqData, Context) + end + end. + +decode(Keys, Body) -> + case decode(Body) of + {ok, J0} -> + J = maps:fold(fun(K, V, Acc) -> + Acc#{binary_to_atom(K, utf8) => V} + end, J0, J0), + Results = [get_or_missing(K, J) || K <- Keys], + case [E || E = {key_missing, _} <- Results] of + [] -> {ok, Results, J}; + Errors -> {error, Errors} + end; + Else -> Else + end. + +-type parsed_json() :: map() | atom() | binary(). +-spec decode(binary()) -> {ok, parsed_json()} | {error, term()}. + +decode(<<"">>) -> + {ok, #{}}; +%% some HTTP API clients include "null" for payload in certain scenarios +decode(<<"null">>) -> + {ok, #{}}; +decode(<<"undefined">>) -> + {ok, #{}}; +decode(Body) -> + try + case rabbit_json:decode(Body) of + Val when is_map(Val) -> + {ok, Val}; + Val when is_atom(Val) -> + {ok, #{}}; + %% handle double encoded JSON, see rabbitmq/rabbitmq-management#839 + Bin when is_binary(Bin) -> + {error, "invalid payload: the request body JSON-decoded to a string. " + "Is the input doubly-JSON-encoded?"}; + _ -> + {error, not_json} + end + catch error:_ -> {error, not_json} + end. + +get_or_missing(K, L) -> + case maps:get(K, L, undefined) of + undefined -> {key_missing, K}; + V -> V + end. + +get_node(Props) -> + case maps:get(<<"node">>, Props, undefined) of + undefined -> node(); + N -> rabbit_nodes:make( + binary_to_list(N)) + end. + +direct_request(MethodName, Transformers, Extra, ErrorMsg, ReqData, + Context = #context{user = User}) -> + with_vhost_and_props( + fun(VHost, Props, ReqData1) -> + Method = props_to_method(MethodName, Props, Transformers, Extra), + Node = get_node(Props), + case rabbit_misc:rpc_call(Node, rabbit_channel, handle_method, + [Method, none, #{}, none, + VHost, User]) of + {badrpc, nodedown} -> + Msg = io_lib:format("Node ~p could not be contacted", [Node]), + rabbit_log:warning(ErrorMsg, [Msg]), + bad_request(list_to_binary(Msg), ReqData1, Context); + {badrpc, {'EXIT', #amqp_error{name = not_found, explanation = Explanation}}} -> + rabbit_log:warning(ErrorMsg, [Explanation]), + not_found(Explanation, ReqData1, Context); + {badrpc, {'EXIT', #amqp_error{name = access_refused, explanation = Explanation}}} -> + rabbit_log:warning(ErrorMsg, [Explanation]), + not_authorised(<<"Access refused.">>, ReqData1, Context); + {badrpc, {'EXIT', #amqp_error{name = not_allowed, explanation = Explanation}}} -> + rabbit_log:warning(ErrorMsg, [Explanation]), + not_authorised(<<"Access refused.">>, ReqData1, Context); + {badrpc, {'EXIT', #amqp_error{explanation = Explanation}}} -> + rabbit_log:warning(ErrorMsg, [Explanation]), + bad_request(list_to_binary(Explanation), ReqData1, Context); + {badrpc, Reason} -> + rabbit_log:warning(ErrorMsg, [Reason]), + bad_request( + list_to_binary( + io_lib:format("Request to node ~s failed with ~p", + [Node, Reason])), + ReqData1, Context); + _ -> {true, ReqData1, Context} + end + end, ReqData, Context). + +with_vhost_and_props(Fun, ReqData, Context) -> + case vhost(ReqData) of + not_found -> + not_found(rabbit_data_coercion:to_binary("vhost_not_found"), + ReqData, Context); + VHost -> + {ok, Body, ReqData1} = read_complete_body(ReqData), + case decode(Body) of + {ok, Props} -> + try + Fun(VHost, Props, ReqData1) + catch {error, Error} -> + bad_request(Error, ReqData1, Context) + end; + {error, Reason} -> + bad_request(rabbit_mgmt_format:escape_html_tags(Reason), + ReqData1, Context) + end + end. + +props_to_method(MethodName, Props, Transformers, Extra) when Props =:= null orelse + Props =:= undefined -> + props_to_method(MethodName, #{}, Transformers, Extra); +props_to_method(MethodName, Props, Transformers, Extra) -> + Props1 = [{list_to_atom(binary_to_list(K)), V} || {K, V} <- maps:to_list(Props)], + props_to_method( + MethodName, rabbit_mgmt_format:format(Props1 ++ Extra, {Transformers, true})). + +props_to_method(MethodName, Props) -> + Props1 = rabbit_mgmt_format:format( + Props, + {fun rabbit_mgmt_format:format_args/1, true}), + FieldNames = ?FRAMING:method_fieldnames(MethodName), + {Res, _Idx} = lists:foldl( + fun (K, {R, Idx}) -> + NewR = case pget(K, Props1) of + undefined -> R; + V -> setelement(Idx, R, V) + end, + {NewR, Idx + 1} + end, {?FRAMING:method_record(MethodName), 2}, + FieldNames), + Res. + +parse_bool(V) -> rabbit_misc:parse_bool(V). + +parse_int(V) -> rabbit_misc:parse_int(V). + +with_channel(VHost, ReqData, Context, Fun) -> + with_channel(VHost, ReqData, Context, node(), Fun). + +with_channel(VHost, ReqData, + Context = #context{user = #user {username = Username}, + password = Password}, + Node, Fun) -> + Params = #amqp_params_direct{username = Username, + password = Password, + node = Node, + virtual_host = VHost}, + case amqp_connection:start(Params) of + {ok, Conn} -> + {ok, Ch} = amqp_connection:open_channel(Conn), + try + Fun(Ch) + catch + exit:{{shutdown, + {server_initiated_close, ?NOT_FOUND, Reason}}, _} -> + not_found(Reason, ReqData, Context); + exit:{{shutdown, + {server_initiated_close, ?ACCESS_REFUSED, Reason}}, _} -> + not_authorised(Reason, ReqData, Context); + exit:{{shutdown, {ServerClose, Code, Reason}}, _} + when ServerClose =:= server_initiated_close; + ServerClose =:= server_initiated_hard_close -> + bad_request_exception(Code, Reason, ReqData, Context); + exit:{{shutdown, {connection_closing, + {ServerClose, Code, Reason}}}, _} + when ServerClose =:= server_initiated_close; + ServerClose =:= server_initiated_hard_close -> + bad_request_exception(Code, Reason, ReqData, Context) + after + catch amqp_channel:close(Ch), + catch amqp_connection:close(Conn) + end; + {error, {auth_failure, Msg}} -> + not_authorised(Msg, ReqData, Context); + {error, not_allowed} -> + not_authorised(<<"Access refused.">>, ReqData, Context); + {error, access_refused} -> + not_authorised(<<"Access refused.">>, ReqData, Context); + {error, {nodedown, N}} -> + bad_request( + list_to_binary( + io_lib:format("Node ~s could not be contacted", [N])), + ReqData, Context) + end. + +bad_request_exception(Code, Reason, ReqData, Context) -> + bad_request(list_to_binary(io_lib:format("~p ~s", [Code, Reason])), + ReqData, Context). + +all_or_one_vhost(ReqData, Fun) -> + case vhost(ReqData) of + none -> lists:append([Fun(V) || V <- rabbit_vhost:list_names()]); + not_found -> vhost_not_found; + VHost -> Fun(VHost) + end. + +filter_vhost(List, ReqData, Context) -> + User = #user{tags = Tags} = Context#context.user, + Fn = case is_admin(Tags) of + true -> fun list_visible_vhosts_names/2; + false -> fun list_login_vhosts_names/2 + end, + AuthzData = get_authz_data(ReqData), + VHosts = Fn(User, AuthzData), + [I || I <- List, lists:member(pget(vhost, I), VHosts)]. + +filter_user(List, _ReqData, #context{user = User}) -> + filter_user(List, User). + +filter_user(List, #user{username = Username, tags = Tags}) -> + case is_monitor(Tags) of + true -> List; + false -> [I || I <- List, pget(user, I) == Username] + end. + +filter_conn_ch_list(List, ReqData, Context) -> + rabbit_mgmt_format:strip_pids( + filter_user( + case vhost(ReqData) of + none -> List; + VHost -> [I || I <- List, pget(vhost, I) =:= VHost] + end, ReqData, Context)). + +filter_tracked_conn_list(List, ReqData, #context{user = #user{username = FilterUsername, tags = Tags}}) -> + %% A bit of duplicated code, but we only go through the list once + case {vhost(ReqData), is_monitor(Tags)} of + {none, true} -> + [[{name, Name}, + {vhost, VHost}, + {user, Username}, + {node, Node}] || #tracked_connection{name = Name, vhost = VHost, username = Username, node = Node} <- List]; + {FilterVHost, true} -> + [[{name, Name}, + {vhost, VHost}, + {user, Username}, + {node, Node}] || #tracked_connection{name = Name, vhost = VHost, username = Username, node = Node} <- List, VHost == FilterVHost]; + {none, false} -> + [[{name, Name}, + {vhost, VHost}, + {user, Username}, + {node, Node}] || #tracked_connection{name = Name, vhost = VHost, username = Username, node = Node} <- List, Username == FilterUsername]; + {FilterVHost, false} -> + [[{name, Name}, + {vhost, VHost}, + {user, Username}, + {node, Node}] || #tracked_connection{name = Name, vhost = VHost, username = Username, node = Node} <- List, VHost == FilterVHost, Username == FilterUsername] + end. + +set_resp_header(K, V, ReqData) -> + cowboy_req:set_resp_header(K, strip_crlf(V), ReqData). + +strip_crlf(Str) -> lists:append(string:tokens(Str, "\r\n")). + +args([]) -> args(#{}); +args(L) -> rabbit_mgmt_format:to_amqp_table(L). + +%% Make replying to a post look like anything else... +post_respond({true, ReqData, Context}) -> + {true, ReqData, Context}; +post_respond({stop, ReqData, Context}) -> + {stop, ReqData, Context}; +post_respond({JSON, ReqData, Context}) -> + {true, cowboy_req:set_resp_body(JSON, ReqData), Context}. + +is_admin(T) -> intersects(T, [administrator]). +is_policymaker(T) -> intersects(T, [administrator, policymaker]). +is_monitor(T) -> intersects(T, [administrator, monitoring]). +is_mgmt_user(T) -> intersects(T, [administrator, monitoring, policymaker, + management]). + +intersects(A, B) -> lists:any(fun(I) -> lists:member(I, B) end, A). + +%% The distinction between list_visible_vhosts and list_login_vhosts +%% is there to ensure that monitors can always learn of the +%% existence of all vhosts, and can always see their contribution to +%% global stats. + +list_visible_vhosts_names(User) -> + list_visible_vhosts(User, undefined). + +list_visible_vhosts_names(User, AuthzData) -> + list_visible_vhosts(User, AuthzData). + +list_visible_vhosts(User) -> + list_visible_vhosts(User, undefined). + +list_visible_vhosts(User = #user{tags = Tags}, AuthzData) -> + case is_monitor(Tags) of + true -> rabbit_vhost:list_names(); + false -> list_login_vhosts_names(User, AuthzData) + end. + +list_login_vhosts_names(User, AuthzData) -> + [V || V <- rabbit_vhost:list_names(), + case catch rabbit_access_control:check_vhost_access(User, V, AuthzData, #{}) of + ok -> true; + NotOK -> + log_access_control_result(NotOK), + false + end]. + +list_login_vhosts(User, AuthzData) -> + [V || V <- rabbit_vhost:all(), + case catch rabbit_access_control:check_vhost_access(User, vhost:get_name(V), AuthzData, #{}) of + ok -> true; + NotOK -> + log_access_control_result(NotOK), + false + end]. + +% rabbitmq/rabbitmq-auth-backend-http#100 +log_access_control_result(NotOK) -> + rabbit_log:debug("rabbit_access_control:check_vhost_access result: ~p", [NotOK]). + +%% base64:decode throws lots of weird errors. Catch and convert to one +%% that will cause a bad_request. +b64decode_or_throw(B64) -> + rabbit_misc:b64decode_or_throw(B64). + +no_range() -> {no_range, no_range, no_range, no_range}. + +%% Take floor on queries so we make sure we only return samples +%% for which we've finished receiving events. Fixes the "drop at +%% the end" problem. +range(ReqData) -> {range("lengths", fun floor/2, ReqData), + range("msg_rates", fun floor/2, ReqData), + range("data_rates", fun floor/2, ReqData), + range("node_stats", fun floor/2, ReqData)}. + +%% ...but if we know only one event could have contributed towards +%% what we are interested in, then let's take the ceiling instead and +%% get slightly fresher data that will match up with any +%% non-historical data we have (e.g. queue length vs queue messages in +%% RAM, they should both come from the same snapshot or we might +%% report more messages in RAM than total). +%% +%% However, we only do this for queue lengths since a) it's the only +%% thing where this ends up being really glaring and b) for other +%% numbers we care more about the rate than the absolute value, and if +%% we use ceil() we stand a 50:50 chance of looking up the last sample +%% in the range before we get it, and thus deriving an instantaneous +%% rate of 0.0. +%% +%% Age is assumed to be > 0, Incr > 0 and (Age div Incr) <= ?MAX_RANGE. +%% The latter condition allows us to limit the number of samples that +%% will be sent to the client. +range_ceil(ReqData) -> {range("lengths", fun ceil/2, ReqData), + range("msg_rates", fun floor/2, ReqData), + range("data_rates", fun floor/2, ReqData), + range("node_stats", fun floor/2, ReqData)}. + +range(Prefix, Round, ReqData) -> + Age0 = int(Prefix ++ "_age", ReqData), + Incr0 = int(Prefix ++ "_incr", ReqData), + if + is_atom(Age0) orelse is_atom(Incr0) -> no_range; + (Age0 > 0) andalso (Incr0 > 0) andalso ((Age0 div Incr0) =< ?MAX_RANGE) -> + Age = Age0 * 1000, + Incr = Incr0 * 1000, + Now = os:system_time(milli_seconds), + Last = Round(Now, Incr), + #range{first = (Last - Age), + last = Last, + incr = Incr}; + true -> throw({error, invalid_range_parameters, + io_lib:format("Invalid range parameters: age ~p, incr ~p", + [Age0, Incr0])}) + end. + +floor(TS, Interval) -> (TS div Interval) * Interval. + +ceil(TS, Interval) -> case floor(TS, Interval) of + TS -> TS; + Floor -> Floor + Interval + end. + +ceil(X) when X < 0 -> + trunc(X); +ceil(X) -> + T = trunc(X), + case X - T == 0 of + true -> T; + false -> T + 1 + end. + +int(Name, ReqData) -> + case qs_val(list_to_binary(Name), ReqData) of + undefined -> undefined; + Bin -> case catch list_to_integer(binary_to_list(Bin)) of + {'EXIT', _} -> undefined; + Integer -> Integer + end + end. + +def(undefined, Def) -> Def; +def(V, _) -> V. + +-spec qs_val(binary(), cowboy:req()) -> any() | undefined. +qs_val(Name, ReqData) -> + Qs = cowboy_req:parse_qs(ReqData), + proplists:get_value(Name, Qs, undefined). + +-spec catch_no_such_user_or_vhost(fun(() -> Result), Replacement) -> Result | Replacement. +catch_no_such_user_or_vhost(Fun, Replacement) -> + try + Fun() + catch throw:{error, {E, _}} when E =:= no_such_user; E =:= no_such_vhost -> + Replacement() + end. + +%% this retains the old, buggy, pre 23.1 behavour of lists:sublist/3 where an +%% error is thrown when the request is out of range +sublist(List, S, L) when is_integer(L), L >= 0 -> + lists:sublist(lists:nthtail(S-1, List), L). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_aliveness_test.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_aliveness_test.erl new file mode 100644 index 0000000000..992ec954e2 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_aliveness_test.erl @@ -0,0 +1,71 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_aliveness_test). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([resource_exists/2]). +-export([variances/2]). + +-include("rabbit_mgmt.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). + +-define(QUEUE, <<"aliveness-test">>). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case rabbit_mgmt_util:vhost(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:with_channel( + rabbit_mgmt_util:vhost(ReqData), + ReqData, + Context, + fun(Ch) -> + #'queue.declare_ok'{queue = ?QUEUE} = amqp_channel:call(Ch, #'queue.declare'{ + queue = ?QUEUE + }), + ok = amqp_channel:call(Ch, #'basic.publish'{routing_key = ?QUEUE}, #amqp_msg{ + payload = <<"test_message">> + }), + case amqp_channel:call(Ch, #'basic.get'{queue = ?QUEUE, no_ack = true}) of + {#'basic.get_ok'{}, _} -> + %% Don't delete the queue. If this is pinged every few + %% seconds we don't want to create a mnesia transaction + %% each time. + rabbit_mgmt_util:reply([{status, ok}], ReqData, Context); + #'basic.get_empty'{} -> + Reason = <<"aliveness-test queue is empty">>, + failure(Reason, ReqData, Context); + Error -> + Reason = rabbit_data_coercion:to_binary(Error), + failure(Reason, ReqData, Context) + end + end + ). + +failure(Reason, ReqData0, Context0) -> + Body = #{status => failed, reason => Reason}, + {Response, ReqData1, Context1} = rabbit_mgmt_util:reply(Body, ReqData0, Context0), + {stop, cowboy_req:reply(?HEALTH_CHECK_FAILURE_STATUS, #{}, Response, ReqData1), Context1}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_vhost(ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_auth.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_auth.erl new file mode 100644 index 0000000000..6899fc54ee --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_auth.erl @@ -0,0 +1,51 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_auth). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +to_json(ReqData, Context) -> + EnableUAA = application:get_env(rabbitmq_management, enable_uaa, false), + Data = case EnableUAA of + true -> + UAAClientId = application:get_env(rabbitmq_management, uaa_client_id, ""), + UAALocation = application:get_env(rabbitmq_management, uaa_location, ""), + case is_invalid([UAAClientId, UAALocation]) of + true -> + rabbit_log:warning("Disabling OAuth 2 authorization, relevant configuration settings are missing", []), + [{enable_uaa, false}, {uaa_client_id, <<>>}, {uaa_location, <<>>}]; + false -> + [{enable_uaa, true}, + {uaa_client_id, rabbit_data_coercion:to_binary(UAAClientId)}, + {uaa_location, rabbit_data_coercion:to_binary(UAALocation)}] + end; + false -> + [{enable_uaa, false}, {uaa_client_id, <<>>}, {uaa_location, <<>>}] + end, + rabbit_mgmt_util:reply(Data, ReqData, Context). + +is_authorized(ReqData, Context) -> + {true, ReqData, Context}. + +is_invalid(List) -> + lists:any(fun(V) -> V == "" end, List). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_auth_attempts.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_auth_attempts.erl new file mode 100644 index 0000000000..cdd11826ea --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_auth_attempts.erl @@ -0,0 +1,79 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_auth_attempts). + +-export([init/2, to_json/2, content_types_provided/2, allowed_methods/2, is_authorized/2, + delete_resource/2, resource_exists/2]). +-export([variances/2]). + +-import(rabbit_misc, [pget/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- +init(Req, [Mode]) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), + {Mode, #context{}}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {node_exists(ReqData, get_node(ReqData)), ReqData, Context}. + +to_json(ReqData, {Mode, Context}) -> + rabbit_mgmt_util:reply(augment(Mode, ReqData), ReqData, Context). + +is_authorized(ReqData, {Mode, Context}) -> + {Res, Req2, Context2} = rabbit_mgmt_util:is_authorized_monitor(ReqData, Context), + {Res, Req2, {Mode, Context2}}. + +delete_resource(ReqData, Context) -> + Node = get_node(ReqData), + case node_exists(ReqData, Node) of + false -> + {false, ReqData, Context}; + true -> + case rpc:call(Node, rabbit_core_metrics, reset_auth_attempt_metrics, [], infinity) of + {badrpc, _} -> {false, ReqData, Context}; + ok -> {true, ReqData, Context} + end + end. +%%-------------------------------------------------------------------- +get_node(ReqData) -> + list_to_atom(binary_to_list(rabbit_mgmt_util:id(node, ReqData))). + +node_exists(ReqData, Node) -> + case [N || N <- rabbit_mgmt_wm_nodes:all_nodes(ReqData), + proplists:get_value(name, N) == Node] of + [] -> false; + [_] -> true + end. + +augment(Mode, ReqData) -> + Node = get_node(ReqData), + case node_exists(ReqData, Node) of + false -> + not_found; + true -> + Fun = case Mode of + all -> get_auth_attempts; + by_source -> get_auth_attempts_by_source + end, + case rpc:call(Node, rabbit_core_metrics, Fun, [], infinity) of + {badrpc, _} -> not_available; + Result -> Result + end + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_binding.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_binding.erl new file mode 100644 index 0000000000..4396edc53b --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_binding.erl @@ -0,0 +1,141 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_binding). + +-export([init/2, resource_exists/2, to_json/2, + content_types_provided/2, content_types_accepted/2, + is_authorized/2, allowed_methods/2, delete_resource/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). + +%%-------------------------------------------------------------------- +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + Binding = binding(ReqData), + {case Binding of + not_found -> false; + {bad_request, _} -> false; + _ -> case rabbit_binding:exists(Binding) of + true -> true; + _ -> false + end + end, ReqData, Context}. + +to_json(ReqData, Context) -> + with_binding(ReqData, Context, + fun(Binding) -> + rabbit_mgmt_util:reply( + rabbit_mgmt_format:binding(Binding), + ReqData, Context) + end). + +delete_resource(ReqData, Context) -> + MethodName = case rabbit_mgmt_util:destination_type(ReqData) of + exchange -> 'exchange.unbind'; + queue -> 'queue.unbind' + end, + with_binding( + ReqData, Context, + fun(#binding{ source = #resource{name = S}, + destination = #resource{name = D}, + key = Key, + args = Args }) -> + rabbit_mgmt_util:direct_request( + MethodName, + fun rabbit_mgmt_format:format_accept_content/1, + [{queue, D}, + {exchange, S}, + {destination, D}, + {source, S}, + {routing_key, Key}, + {arguments, Args}], + "Unbinding error: ~s", ReqData, Context) + end). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_vhost(ReqData, Context). + +%%-------------------------------------------------------------------- + +binding(ReqData) -> + case rabbit_mgmt_util:vhost(ReqData) of + not_found -> not_found; + VHost -> Source = rabbit_mgmt_util:id(source, ReqData), + Dest = rabbit_mgmt_util:id(destination, ReqData), + DestType = rabbit_mgmt_util:destination_type(ReqData), + Props = rabbit_mgmt_util:id(props, ReqData), + SName = rabbit_misc:r(VHost, exchange, Source), + DName = rabbit_misc:r(VHost, DestType, Dest), + case unpack(SName, DName, Props) of + {bad_request, Str} -> + {bad_request, Str}; + {Key, Args} -> + #binding{ source = SName, + destination = DName, + key = Key, + args = Args } + end + end. + +unpack(Src, Dst, Props) -> + case rabbit_mgmt_format:tokenise(binary_to_list(Props)) of + ["\~"] -> {<<>>, []}; + %% when routing_key is explicitly set to `null` in JSON payload, + %% the value would be stored as null the atom. See rabbitmq/rabbitmq-management#723 for details. + ["null"] -> {null, []}; + ["undefined"] -> {undefined, []}; + [Key] -> {unquote(Key), []}; + ["\~", ArgsEnc] -> lookup(<<>>, ArgsEnc, Src, Dst); + %% see above + ["null", ArgsEnc] -> lookup(null, ArgsEnc, Src, Dst); + ["undefined", ArgsEnc] -> lookup(undefined, ArgsEnc, Src, Dst); + [Key, ArgsEnc] -> lookup(unquote(Key), ArgsEnc, Src, Dst); + _ -> {bad_request, {too_many_tokens, Props}} + end. + +lookup(RoutingKey, ArgsEnc, Src, Dst) -> + lookup(RoutingKey, unquote(ArgsEnc), + rabbit_binding:list_for_source_and_destination(Src, Dst)). + +lookup(_RoutingKey, _Hash, []) -> + {bad_request, "binding not found"}; +lookup(RoutingKey, Hash, [#binding{args = Args} | Rest]) -> + case args_hash(Args) =:= Hash of + true -> {RoutingKey, Args}; + false -> lookup(RoutingKey, Hash, Rest) + end. + +args_hash(Args) -> + rabbit_mgmt_format:args_hash(Args). + +unquote(Name) -> + list_to_binary(rabbit_http_util:unquote(Name)). + +with_binding(ReqData, Context, Fun) -> + case binding(ReqData) of + {bad_request, Reason} -> + rabbit_mgmt_util:bad_request(Reason, ReqData, Context); + Binding -> + Fun(Binding) + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_bindings.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_bindings.erl new file mode 100644 index 0000000000..5ed0a82f8d --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_bindings.erl @@ -0,0 +1,149 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_bindings). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([allowed_methods/2]). +-export([content_types_accepted/2, accept_content/2, resource_exists/2]). +-export([basic/1, augmented/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, [Mode]) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), {Mode, #context{}}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +%% The current version of Cowboy forces us to report the resource doesn't +%% exist here in order to get a 201 response. It seems Cowboy confuses the +%% resource from the request and the resource that will be created by POST. +%% https://github.com/ninenines/cowboy/issues/723#issuecomment-161319576 +resource_exists(ReqData, {Mode, Context}) -> + case cowboy_req:method(ReqData) of + <<"POST">> -> + {false, ReqData, {Mode, Context}}; + _ -> + {case list_bindings(Mode, ReqData) of + vhost_not_found -> false; + _ -> true + end, ReqData, {Mode, Context}} + end. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +%% Methods to add to the CORS header. +%% This clause is called by rabbit_mgmt_cors:handle_options/2 +allowed_methods(undefined, undefined) -> + {[<<"HEAD">>, <<"GET">>, <<"POST">>, <<"OPTIONS">>], undefined, undefined}; +allowed_methods(ReqData, {Mode, Context}) -> + {case Mode of + source_destination -> [<<"HEAD">>, <<"GET">>, <<"POST">>, <<"OPTIONS">>]; + _ -> [<<"HEAD">>, <<"GET">>, <<"OPTIONS">>] + end, ReqData, {Mode, Context}}. + +to_json(ReqData, {Mode, Context}) -> + Bs = [rabbit_mgmt_format:binding(B) || B <- list_bindings(Mode, ReqData)], + rabbit_mgmt_util:reply_list( + rabbit_mgmt_util:filter_vhost(Bs, ReqData, Context), + ["vhost", "source", "type", "destination", + "routing_key", "properties_key"], + ReqData, {Mode, Context}). + +accept_content(ReqData0, {_Mode, Context}) -> + {ok, Body, ReqData} = rabbit_mgmt_util:read_complete_body(ReqData0), + Source = rabbit_mgmt_util:id(source, ReqData), + Dest = rabbit_mgmt_util:id(destination, ReqData), + DestType = rabbit_mgmt_util:id(dtype, ReqData), + VHost = rabbit_mgmt_util:vhost(ReqData), + {ok, Props} = rabbit_mgmt_util:decode(Body), + MethodName = case rabbit_mgmt_util:destination_type(ReqData) of + exchange -> 'exchange.bind'; + queue -> 'queue.bind' + end, + {Key, Args} = key_args(DestType, Props), + case rabbit_mgmt_util:direct_request( + MethodName, + fun rabbit_mgmt_format:format_accept_content/1, + [{queue, Dest}, + {exchange, Source}, + {destination, Dest}, + {source, Source}, + {routing_key, Key}, + {arguments, Args}], + "Binding error: ~s", ReqData, Context) of + {stop, _, _} = Res -> + Res; + {true, ReqData, Context2} -> + From = binary_to_list(cowboy_req:path(ReqData)), + Prefix = rabbit_mgmt_util:get_path_prefix(), + BindingProps = rabbit_mgmt_format:pack_binding_props(Key, Args), + UrlWithBindings = rabbit_mgmt_format:url("/api/bindings/~s/e/~s/~s/~s/~s", + [VHost, Source, DestType, + Dest, BindingProps]), + To = Prefix ++ binary_to_list(UrlWithBindings), + Loc = rabbit_web_dispatch_util:relativise(From, To), + {{true, Loc}, ReqData, Context2} + end. + +is_authorized(ReqData, {Mode, Context}) -> + {Res, RD2, C2} = rabbit_mgmt_util:is_authorized_vhost(ReqData, Context), + {Res, RD2, {Mode, C2}}. + +%%-------------------------------------------------------------------- + +basic(ReqData) -> + [rabbit_mgmt_format:binding(B) || + B <- list_bindings(all, ReqData)]. + +augmented(ReqData, Context) -> + rabbit_mgmt_util:filter_vhost(basic(ReqData), ReqData, Context). + +key_args(<<"q">>, Props) -> + #'queue.bind'{routing_key = K, arguments = A} = + rabbit_mgmt_util:props_to_method( + 'queue.bind', Props, [], []), + {K, A}; + +key_args(<<"e">>, Props) -> + #'exchange.bind'{routing_key = K, arguments = A} = + rabbit_mgmt_util:props_to_method( + 'exchange.bind', Props, + [], []), + {K, A}. + +%%-------------------------------------------------------------------- + +list_bindings(all, ReqData) -> + rabbit_mgmt_util:all_or_one_vhost(ReqData, + fun (VHost) -> + rabbit_binding:list(VHost) + end); +list_bindings(exchange_source, ReqData) -> + rabbit_binding:list_for_source(r(exchange, exchange, ReqData)); +list_bindings(exchange_destination, ReqData) -> + rabbit_binding:list_for_destination(r(exchange, exchange, ReqData)); +list_bindings(queue, ReqData) -> + rabbit_binding:list_for_destination(r(queue, destination, ReqData)); +list_bindings(source_destination, ReqData) -> + DestType = rabbit_mgmt_util:destination_type(ReqData), + rabbit_binding:list_for_source_and_destination( + r(exchange, source, ReqData), + r(DestType, destination, ReqData)). + +r(Type, Name, ReqData) -> + rabbit_misc:r(rabbit_mgmt_util:vhost(ReqData), Type, + rabbit_mgmt_util:id(Name, ReqData)). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_channel.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_channel.erl new file mode 100644 index 0000000000..0b4aae6c13 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_channel.erl @@ -0,0 +1,68 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_channel). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([resource_exists/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + case channel(ReqData) of + not_found -> {false, ReqData, Context}; + _Conn -> {true, ReqData, Context} + end; + true -> + {false, ReqData, Context} + end. + +to_json(ReqData, Context) -> + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + Payload = rabbit_mgmt_format:clean_consumer_details( + rabbit_mgmt_format:strip_pids(channel(ReqData))), + rabbit_mgmt_util:reply( + maps:from_list(Payload), + ReqData, Context); + true -> + rabbit_mgmt_util:bad_request(<<"Stats in management UI are disabled on this node">>, ReqData, Context) + end. + +is_authorized(ReqData, Context) -> + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + try + rabbit_mgmt_util:is_authorized_user(ReqData, Context, channel(ReqData)) + catch + {error, invalid_range_parameters, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, Context) + end; + true -> + rabbit_mgmt_util:bad_request(<<"Stats in management UI are disabled on this node">>, ReqData, Context) + end. + +%%-------------------------------------------------------------------- + +channel(ReqData) -> + rabbit_mgmt_db:get_channel(rabbit_mgmt_util:id(channel, ReqData), + rabbit_mgmt_util:range(ReqData)). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_channels.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_channels.erl new file mode 100644 index 0000000000..a064b2f9f9 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_channels.erl @@ -0,0 +1,50 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_channels). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2, + augmented/2]). +-export([variances/2]). + +-import(rabbit_misc, [pget/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +to_json(ReqData, Context) -> + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + try + rabbit_mgmt_util:reply_list_or_paginate(augmented(ReqData, Context), + ReqData, Context) + catch + {error, invalid_range_parameters, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, Context) + end; + true -> + rabbit_mgmt_util:bad_request(<<"Stats in management UI are disabled on this node">>, ReqData, Context) + end. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). + +augmented(ReqData, Context) -> + rabbit_mgmt_util:filter_conn_ch_list( + rabbit_mgmt_db:get_all_channels( + rabbit_mgmt_util:range(ReqData)), ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_channels_vhost.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_channels_vhost.erl new file mode 100644 index 0000000000..454a29e0bb --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_channels_vhost.erl @@ -0,0 +1,54 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2010-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_channels_vhost). + +%% Lists channels in a vhost + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2, + augmented/2, resource_exists/2]). +-export([variances/2]). + +-import(rabbit_misc, [pget/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {rabbit_vhost:exists(rabbit_mgmt_util:id(vhost, ReqData)), ReqData, Context}. + +to_json(ReqData, Context) -> + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + try + rabbit_mgmt_util:reply_list(augmented(ReqData, Context), ReqData, Context) + catch + {error, invalid_range_parameters, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, Context) + end; + true -> + rabbit_mgmt_util:bad_request(<<"Stats in management UI are disabled on this node">>, ReqData, Context) + end. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_vhost(ReqData, Context). + +augmented(ReqData, Context) -> + rabbit_mgmt_util:filter_conn_ch_list( + rabbit_mgmt_db:get_all_channels( + rabbit_mgmt_util:range(ReqData)), ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_cluster_name.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_cluster_name.erl new file mode 100644 index 0000000000..132da93ad2 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_cluster_name.erl @@ -0,0 +1,59 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_cluster_name). + +-export([init/2, resource_exists/2, to_json/2, + content_types_provided/2, content_types_accepted/2, + is_authorized/2, allowed_methods/2, accept_content/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"PUT">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {true, ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply( + [{name, rabbit_nodes:cluster_name()}], ReqData, Context). + +accept_content(ReqData0, Context = #context{user = #user{username = Username}}) -> + rabbit_mgmt_util:with_decode( + [name], ReqData0, Context, fun([Name], _, ReqData) -> + rabbit_nodes:set_cluster_name( + as_binary(Name), Username), + {true, ReqData, Context} + end). + +is_authorized(ReqData, Context) -> + case cowboy_req:method(ReqData) of + <<"PUT">> -> rabbit_mgmt_util:is_authorized_admin(ReqData, Context); + _ -> rabbit_mgmt_util:is_authorized(ReqData, Context) + end. + +as_binary(Val) when is_binary(Val) -> + Val; +as_binary(Val) when is_list(Val) -> + list_to_binary(Val). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_connection.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_connection.erl new file mode 100644 index 0000000000..7de605e6a5 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_connection.erl @@ -0,0 +1,98 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_connection). + +-export([init/2, resource_exists/2, to_json/2, content_types_provided/2, + is_authorized/2, allowed_methods/2, delete_resource/2, conn/1]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + case conn(ReqData) of + not_found -> {false, ReqData, Context}; + _Conn -> {true, ReqData, Context} + end. + +to_json(ReqData, Context) -> + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + rabbit_mgmt_util:reply( + maps:from_list(rabbit_mgmt_format:strip_pids(conn_stats(ReqData))), ReqData, Context); + true -> + rabbit_mgmt_util:reply([{name, rabbit_mgmt_util:id(connection, ReqData)}], + ReqData, Context) + end. + +delete_resource(ReqData, Context) -> + case conn(ReqData) of + not_found -> ok; + Conn -> + case proplists:get_value(pid, Conn) of + undefined -> ok; + Pid when is_pid(Pid) -> + force_close_connection(ReqData, Conn, Pid) + end + end, + {true, ReqData, Context}. + +is_authorized(ReqData, Context) -> + try + rabbit_mgmt_util:is_authorized_user(ReqData, Context, conn(ReqData)) + catch + {error, invalid_range_parameters, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, Context) + end. + +%%-------------------------------------------------------------------- + +conn(ReqData) -> + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + conn_stats(ReqData); + true -> + case rabbit_connection_tracking:lookup(rabbit_mgmt_util:id(connection, ReqData)) of + #tracked_connection{name = Name, pid = Pid, username = Username, type = Type} -> + [{name, Name}, {pid, Pid}, {user, Username}, {type, Type}]; + not_found -> + not_found + end + end. + +conn_stats(ReqData) -> + rabbit_mgmt_db:get_connection(rabbit_mgmt_util:id(connection, ReqData), + rabbit_mgmt_util:range_ceil(ReqData)). + +force_close_connection(ReqData, Conn, Pid) -> + Reason = case cowboy_req:header(<<"x-reason">>, ReqData) of + undefined -> "Closed via management plugin"; + V -> binary_to_list(V) + end, + case proplists:get_value(type, Conn) of + direct -> amqp_direct_connection:server_close(Pid, 320, Reason); + network -> rabbit_networking:close_connection(Pid, Reason); + _ -> + % best effort, this will work for connections to the stream plugin + gen_server:call(Pid, {shutdown, Reason}, infinity) + end, + ok. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_connection_channels.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_connection_channels.erl new file mode 100644 index 0000000000..dfbcfb5ba1 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_connection_channels.erl @@ -0,0 +1,54 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_connection_channels). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([resource_exists/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + case rabbit_mgmt_wm_connection:conn(ReqData) of + error -> {false, ReqData, Context}; + _Conn -> {true, ReqData, Context} + end. + +to_json(ReqData, Context) -> + Name = proplists:get_value(name, rabbit_mgmt_wm_connection:conn(ReqData)), + Chs = rabbit_mgmt_db:get_all_channels(rabbit_mgmt_util:range(ReqData)), + rabbit_mgmt_util:reply_list( + [Ch || Ch <- rabbit_mgmt_util:filter_conn_ch_list(Chs, ReqData, Context), + conn_name(Ch) =:= Name], + ReqData, Context). + +is_authorized(ReqData, Context) -> + try + rabbit_mgmt_util:is_authorized_user( + ReqData, Context, rabbit_mgmt_wm_connection:conn(ReqData)) + catch + {error, invalid_range_parameters, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, Context) + end. + +%%-------------------------------------------------------------------- + +conn_name(Ch) -> + proplists:get_value(name, proplists:get_value(connection_details, Ch)). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_connections.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_connections.erl new file mode 100644 index 0000000000..0e1345abca --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_connections.erl @@ -0,0 +1,54 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_connections). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2, + augmented/2]). +-export([variances/2]). + +-import(rabbit_misc, [pget/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +to_json(ReqData, Context) -> + try + Connections = do_connections_query(ReqData, Context), + rabbit_mgmt_util:reply_list_or_paginate(Connections, ReqData, Context) + catch + {error, invalid_range_parameters, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, Context) + end. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). + +augmented(ReqData, Context) -> + rabbit_mgmt_util:filter_conn_ch_list( + rabbit_mgmt_db:get_all_connections( + rabbit_mgmt_util:range_ceil(ReqData)), ReqData, Context). + +do_connections_query(ReqData, Context) -> + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + augmented(ReqData, Context); + true -> + rabbit_mgmt_util:filter_tracked_conn_list(rabbit_connection_tracking:list(), + ReqData, Context) + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_connections_vhost.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_connections_vhost.erl new file mode 100644 index 0000000000..1840a7ae45 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_connections_vhost.erl @@ -0,0 +1,49 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2010-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_connections_vhost). + +%% Lists connections in a vhost + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2, + augmented/2, resource_exists/2]). +-export([variances/2]). + +-import(rabbit_misc, [pget/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {rabbit_vhost:exists(rabbit_mgmt_util:id(vhost, ReqData)), ReqData, Context}. + +to_json(ReqData, Context) -> + try + rabbit_mgmt_util:reply_list(augmented(ReqData, Context), ReqData, Context) + catch + {error, invalid_range_parameters, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, Context) + end. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_vhost(ReqData, Context). + +augmented(ReqData, Context) -> + rabbit_mgmt_util:filter_conn_ch_list( + rabbit_mgmt_db:get_all_connections( + rabbit_mgmt_util:range_ceil(ReqData)), ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_consumers.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_consumers.erl new file mode 100644 index 0000000000..56945d3a5d --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_consumers.erl @@ -0,0 +1,59 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. + +-module(rabbit_mgmt_wm_consumers). + +-export([init/2, to_json/2, content_types_provided/2, resource_exists/2, + is_authorized/2]). +-export([variances/2]). + +-import(rabbit_misc, [pget/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case rabbit_mgmt_util:vhost(ReqData) of + not_found -> false; + none -> true; % none means `all` + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context = #context{user = User}) -> + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + Arg = case rabbit_mgmt_util:vhost(ReqData) of + none -> all; + VHost -> VHost + end, + Consumers = rabbit_mgmt_format:strip_pids(rabbit_mgmt_db:get_all_consumers(Arg)), + Formatted = [rabbit_mgmt_format:format_consumer_arguments(C) || C <- Consumers], + rabbit_mgmt_util:reply_list( + filter_user(Formatted, User), ReqData, Context); + true -> + rabbit_mgmt_util:bad_request(<<"Stats in management UI are disabled on this node">>, ReqData, Context) + end. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). + +filter_user(List, #user{username = Username, tags = Tags}) -> + case rabbit_mgmt_util:is_monitor(Tags) of + true -> List; + false -> [I || I <- List, + pget(user, pget(channel_details, I)) == Username] + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_definitions.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_definitions.erl new file mode 100644 index 0000000000..c0687993a9 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_definitions.erl @@ -0,0 +1,298 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_definitions). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([content_types_accepted/2, allowed_methods/2, accept_json/2]). +-export([accept_multipart/2]). +-export([variances/2]). + +-export([apply_defs/3, apply_defs/5]). + +-import(rabbit_misc, [pget/2]). + +-include("rabbit_mgmt.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{{<<"application">>, <<"json">>, '*'}, accept_json}, + {{<<"multipart">>, <<"form-data">>, '*'}, accept_multipart}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"POST">>, <<"OPTIONS">>], ReqData, Context}. + +to_json(ReqData, Context) -> + case rabbit_mgmt_util:vhost(ReqData) of + none -> + all_definitions(ReqData, Context); + not_found -> + rabbit_mgmt_util:bad_request(rabbit_data_coercion:to_binary("vhost_not_found"), + ReqData, Context); + VHost -> + vhost_definitions(ReqData, VHost, Context) + end. + +all_definitions(ReqData, Context) -> + Xs = [X || X <- rabbit_mgmt_wm_exchanges:basic(ReqData), + export_exchange(X)], + Qs = [Q || Q <- rabbit_mgmt_wm_queues:basic(ReqData), + export_queue(Q)], + QNames = [{pget(name, Q), pget(vhost, Q)} || Q <- Qs], + Bs = [B || B <- rabbit_mgmt_wm_bindings:basic(ReqData), + export_binding(B, QNames)], + Vsn = rabbit:base_product_version(), + ProductName = rabbit:product_name(), + ProductVersion = rabbit:product_version(), + rabbit_mgmt_util:reply( + [{rabbit_version, rabbit_data_coercion:to_binary(Vsn)}, + {rabbitmq_version, rabbit_data_coercion:to_binary(Vsn)}, + {product_name, rabbit_data_coercion:to_binary(ProductName)}, + {product_version, rabbit_data_coercion:to_binary(ProductVersion)}] ++ + filter( + [{users, rabbit_mgmt_wm_users:users(all)}, + {vhosts, rabbit_mgmt_wm_vhosts:basic()}, + {permissions, rabbit_mgmt_wm_permissions:permissions()}, + {topic_permissions, rabbit_mgmt_wm_topic_permissions:topic_permissions()}, + {parameters, rabbit_mgmt_wm_parameters:basic(ReqData)}, + {global_parameters, rabbit_mgmt_wm_global_parameters:basic()}, + {policies, rabbit_mgmt_wm_policies:basic(ReqData)}, + {queues, Qs}, + {exchanges, Xs}, + {bindings, Bs}]), + case rabbit_mgmt_util:qs_val(<<"download">>, ReqData) of + undefined -> ReqData; + Filename -> rabbit_mgmt_util:set_resp_header( + <<"Content-Disposition">>, + "attachment; filename=" ++ + binary_to_list(Filename), ReqData) + end, + Context). + +accept_json(ReqData0, Context) -> + {ok, Body, ReqData} = rabbit_mgmt_util:read_complete_body(ReqData0), + accept(Body, ReqData, Context). + +vhost_definitions(ReqData, VHost, Context) -> + %% rabbit_mgmt_wm_<>:basic/1 filters by VHost if it is available + Xs = [strip_vhost(X) || X <- rabbit_mgmt_wm_exchanges:basic(ReqData), + export_exchange(X)], + VQs = [Q || Q <- rabbit_mgmt_wm_queues:basic(ReqData), export_queue(Q)], + Qs = [strip_vhost(Q) || Q <- VQs], + QNames = [{pget(name, Q), pget(vhost, Q)} || Q <- VQs], + Bs = [strip_vhost(B) || B <- rabbit_mgmt_wm_bindings:basic(ReqData), + export_binding(B, QNames)], + {ok, Vsn} = application:get_key(rabbit, vsn), + Parameters = [rabbit_mgmt_format:parameter( + rabbit_mgmt_wm_parameters:fix_shovel_publish_properties(P)) + || P <- rabbit_runtime_parameters:list(VHost)], + rabbit_mgmt_util:reply( + [{rabbit_version, rabbit_data_coercion:to_binary(Vsn)}] ++ + filter( + [{parameters, Parameters}, + {policies, rabbit_mgmt_wm_policies:basic(ReqData)}, + {queues, Qs}, + {exchanges, Xs}, + {bindings, Bs}]), + case rabbit_mgmt_util:qs_val(<<"download">>, ReqData) of + undefined -> ReqData; + Filename -> + HeaderVal = "attachment; filename=" ++ binary_to_list(Filename), + rabbit_mgmt_util:set_resp_header(<<"Content-Disposition">>, HeaderVal, ReqData) + end, + Context). + +accept_multipart(ReqData0, Context) -> + {Parts, ReqData} = get_all_parts(ReqData0), + Redirect = get_part(<<"redirect">>, Parts), + Payload = get_part(<<"file">>, Parts), + Resp = {Res, _, _} = accept(Payload, ReqData, Context), + case {Res, Redirect} of + {true, unknown} -> {true, ReqData, Context}; + {true, _} -> {{true, Redirect}, ReqData, Context}; + _ -> Resp + end. + +is_authorized(ReqData, Context) -> + case rabbit_mgmt_util:qs_val(<<"auth">>, ReqData) of + undefined -> + case rabbit_mgmt_util:qs_val(<<"token">>, ReqData) of + undefined -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context); + Token -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context, Token) + end; + Auth -> + is_authorized_qs(ReqData, Context, Auth) + end. + +%% Support for the web UI - it can't add a normal "authorization" +%% header for a file download. +is_authorized_qs(ReqData, Context, Auth) -> + case rabbit_web_dispatch_util:parse_auth_header("Basic " ++ Auth) of + [Username, Password] -> rabbit_mgmt_util:is_authorized_admin( + ReqData, Context, Username, Password); + _ -> {?AUTH_REALM, ReqData, Context} + end. + +%%-------------------------------------------------------------------- + +decode(<<"">>) -> + {ok, #{}}; +decode(Body) -> + try + Decoded = rabbit_json:decode(Body), + Normalised = maps:fold(fun(K, V, Acc) -> + Acc#{binary_to_atom(K, utf8) => V} + end, Decoded, Decoded), + {ok, Normalised} + catch error:_ -> {error, not_json} + end. + +accept(Body, ReqData, Context = #context{user = #user{username = Username}}) -> + %% At this point the request was fully received. + %% There is no point in the idle_timeout anymore. + disable_idle_timeout(ReqData), + case decode(Body) of + {error, E} -> + rabbit_log:error("Encountered an error when parsing definitions: ~p", [E]), + rabbit_mgmt_util:bad_request(rabbit_data_coercion:to_binary("failed_to_parse_json"), + ReqData, Context); + {ok, Map} -> + case rabbit_mgmt_util:vhost(ReqData) of + none -> + case apply_defs(Map, Username) of + {error, E} -> + rabbit_log:error("Encountered an error when importing definitions: ~p", [E]), + rabbit_mgmt_util:bad_request(E, ReqData, Context); + ok -> {true, ReqData, Context} + end; + not_found -> + rabbit_mgmt_util:not_found(rabbit_data_coercion:to_binary("vhost_not_found"), + ReqData, Context); + VHost when is_binary(VHost) -> + case apply_defs(Map, Username, VHost) of + {error, E} -> + rabbit_log:error("Encountered an error when importing definitions: ~p", [E]), + rabbit_mgmt_util:bad_request(E, ReqData, Context); + ok -> {true, ReqData, Context} + end + end + end. + +disable_idle_timeout(#{pid := Pid, streamid := StreamID}) -> + Pid ! {{Pid, StreamID}, {set_options, #{idle_timeout => infinity}}}. + +-spec apply_defs(Map :: #{atom() => any()}, ActingUser :: rabbit_types:username()) -> 'ok' | {error, term()}. + +apply_defs(Body, ActingUser) -> + rabbit_definitions:apply_defs(Body, ActingUser). + +-spec apply_defs(Map :: #{atom() => any()}, ActingUser :: rabbit_types:username(), + VHost :: vhost:name()) -> 'ok' | {error, term()}. + +apply_defs(Body, ActingUser, VHost) -> + rabbit_definitions:apply_defs(Body, ActingUser, VHost). + +-spec apply_defs(Map :: #{atom() => any()}, + ActingUser :: rabbit_types:username(), + SuccessFun :: fun(() -> 'ok'), + ErrorFun :: fun((any()) -> 'ok'), + VHost :: vhost:name()) -> 'ok' | {error, term()}. + +apply_defs(Body, ActingUser, SuccessFun, ErrorFun, VHost) -> + rabbit_definitions:apply_defs(Body, ActingUser, SuccessFun, ErrorFun, VHost). + +get_all_parts(Req) -> + get_all_parts(Req, []). + +get_all_parts(Req0, Acc) -> + case cowboy_req:read_part(Req0) of + {done, Req1} -> + {Acc, Req1}; + {ok, Headers, Req1} -> + Name = case cow_multipart:form_data(Headers) of + {data, N} -> N; + {file, N, _, _} -> N + end, + {ok, Body, Req2} = stream_part_body(Req1, <<>>), + get_all_parts(Req2, [{Name, Body}|Acc]) + end. + +stream_part_body(Req0, Acc) -> + case cowboy_req:read_part_body(Req0) of + {more, Data, Req1} -> + stream_part_body(Req1, <<Acc/binary, Data/binary>>); + {ok, Data, Req1} -> + {ok, <<Acc/binary, Data/binary>>, Req1} + end. + +get_part(Name, Parts) -> + case lists:keyfind(Name, 1, Parts) of + false -> unknown; + {_, Value} -> Value + end. + +export_queue(Queue) -> + pget(owner_pid, Queue) == none. + +export_binding(Binding, Qs) -> + Src = pget(source, Binding), + Dest = pget(destination, Binding), + DestType = pget(destination_type, Binding), + VHost = pget(vhost, Binding), + Src =/= <<"">> + andalso + ( (DestType =:= queue andalso lists:member({Dest, VHost}, Qs)) + orelse (DestType =:= exchange andalso Dest =/= <<"">>) ). + +export_exchange(Exchange) -> + export_name(pget(name, Exchange)). + +export_name(<<>>) -> false; +export_name(<<"amq.", _/binary>>) -> false; +export_name(_Name) -> true. + +%%-------------------------------------------------------------------- + +rw_state() -> + [{users, [name, password_hash, hashing_algorithm, tags, limits]}, + {vhosts, [name]}, + {permissions, [user, vhost, configure, write, read]}, + {topic_permissions, [user, vhost, exchange, write, read]}, + {parameters, [vhost, component, name, value]}, + {global_parameters, [name, value]}, + {policies, [vhost, name, pattern, definition, priority, 'apply-to']}, + {queues, [name, vhost, durable, auto_delete, arguments]}, + {exchanges, [name, vhost, type, durable, auto_delete, internal, + arguments]}, + {bindings, [source, vhost, destination, destination_type, routing_key, + arguments]}]. + +filter(Items) -> + [filter_items(N, V, proplists:get_value(N, rw_state())) || {N, V} <- Items]. + +filter_items(Name, List, Allowed) -> + {Name, [filter_item(I, Allowed) || I <- List]}. + +filter_item(Item, Allowed) -> + [{K, Fact} || {K, Fact} <- Item, lists:member(K, Allowed)]. + +strip_vhost(Item) -> + lists:keydelete(vhost, 1, Item). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_exchange.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_exchange.erl new file mode 100644 index 0000000000..2d5930d1d0 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_exchange.erl @@ -0,0 +1,90 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_exchange). + +-export([init/2, resource_exists/2, to_json/2, + content_types_provided/2, content_types_accepted/2, + is_authorized/2, allowed_methods/2, accept_content/2, + delete_resource/2, exchange/1, exchange/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case exchange(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + try + [X] = rabbit_mgmt_db:augment_exchanges( + [exchange(ReqData)], rabbit_mgmt_util:range(ReqData), full), + rabbit_mgmt_util:reply(rabbit_mgmt_format:strip_pids(X), ReqData, Context) + catch + {error, invalid_range_parameters, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, Context) + end. + +accept_content(ReqData, Context) -> + Name = rabbit_mgmt_util:id(exchange, ReqData), + rabbit_mgmt_util:direct_request( + 'exchange.declare', + fun rabbit_mgmt_format:format_accept_content/1, + [{exchange, Name}], "Declare exchange error: ~s", ReqData, Context). + +delete_resource(ReqData, Context) -> + %% We need to retrieve manually if-unused, as the HTTP API uses '-' + %% while the record uses '_' + Name = id(ReqData), + IfUnused = <<"true">> =:= rabbit_mgmt_util:qs_val(<<"if-unused">>, ReqData), + rabbit_mgmt_util:direct_request( + 'exchange.delete', + fun rabbit_mgmt_format:format_accept_content/1, + [{exchange, Name}, + {if_unused, IfUnused}], "Delete exchange error: ~s", ReqData, Context). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_vhost(ReqData, Context). + +%%-------------------------------------------------------------------- + +exchange(ReqData) -> + case rabbit_mgmt_util:vhost(ReqData) of + not_found -> not_found; + VHost -> exchange(VHost, id(ReqData)) + end. + +exchange(VHost, XName) -> + Name = rabbit_misc:r(VHost, exchange, XName), + case rabbit_exchange:lookup(Name) of + {ok, X} -> rabbit_mgmt_format:exchange( + rabbit_exchange:info(X)); + {error, not_found} -> not_found + end. + +id(ReqData) -> + rabbit_mgmt_util:id(exchange, ReqData). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_exchange_publish.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_exchange_publish.erl new file mode 100644 index 0000000000..7e90b54ba1 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_exchange_publish.erl @@ -0,0 +1,105 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2011-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_exchange_publish). + +-export([init/2, resource_exists/2, is_authorized/2, + allowed_methods/2, content_types_provided/2, accept_content/2, + content_types_accepted/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"POST">>, <<"OPTIONS">>], ReqData, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case rabbit_mgmt_wm_exchange:exchange(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +accept_content(ReqData, Context) -> + rabbit_mgmt_util:post_respond(do_it(ReqData, Context)). + +do_it(ReqData0, Context) -> + VHost = rabbit_mgmt_util:vhost(ReqData0), + X = rabbit_mgmt_util:id(exchange, ReqData0), + rabbit_mgmt_util:with_decode( + [routing_key, properties, payload, payload_encoding], ReqData0, Context, + fun ([RoutingKey, Props0, Payload0, Enc], _, ReqData) when is_binary(Payload0) -> + rabbit_mgmt_util:with_channel( + VHost, ReqData, Context, + fun (Ch) -> + MRef = erlang:monitor(process, Ch), + amqp_channel:register_confirm_handler(Ch, self()), + amqp_channel:register_return_handler(Ch, self()), + amqp_channel:call(Ch, #'confirm.select'{}), + Props = rabbit_mgmt_format:to_basic_properties(Props0), + Payload = decode(Payload0, Enc), + amqp_channel:cast(Ch, #'basic.publish'{ + exchange = X, + routing_key = RoutingKey, + mandatory = true}, + #amqp_msg{props = Props, + payload = Payload}), + receive + {#'basic.return'{}, _} -> + receive + #'basic.ack'{} -> ok + end, + good(MRef, false, ReqData, Context); + #'basic.ack'{} -> + good(MRef, true, ReqData, Context); + #'basic.nack'{} -> + erlang:demonitor(MRef), + bad(rejected, ReqData, Context); + {'DOWN', _, _, _, Err} -> + bad(Err, ReqData, Context) + end + end); + ([_RoutingKey, _Props, _Payload, _Enc], _, _ReqData) -> + throw({error, payload_not_string}) + end). + +good(MRef, Routed, ReqData, Context) -> + erlang:demonitor(MRef), + rabbit_mgmt_util:reply([{routed, Routed}], ReqData, Context). + +bad({shutdown, {connection_closing, + {server_initiated_close, Code, Reason}}}, ReqData, Context) -> + rabbit_mgmt_util:bad_request_exception(Code, Reason, ReqData, Context); + +bad({shutdown, {server_initiated_close, Code, Reason}}, ReqData, Context) -> + rabbit_mgmt_util:bad_request_exception(Code, Reason, ReqData, Context); +bad(rejected, ReqData, Context) -> + Msg = "Unable to publish message. Check queue limits.", + rabbit_mgmt_util:bad_request_exception(rejected, Msg, ReqData, Context). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_vhost(ReqData, Context). + +%%-------------------------------------------------------------------- + +decode(Payload, <<"string">>) -> Payload; +decode(Payload, <<"base64">>) -> rabbit_mgmt_util:b64decode_or_throw(Payload); +decode(_Payload, Enc) -> throw({error, {unsupported_encoding, Enc}}). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_exchanges.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_exchanges.erl new file mode 100644 index 0000000000..9f7abab20f --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_exchanges.erl @@ -0,0 +1,75 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_exchanges). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2, + resource_exists/2, basic/1, augmented/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +-define(DEFAULT_SORT, ["vhost", "name"]). + +-define(BASIC_COLUMNS, ["vhost", "name", "type", "durable", "auto_delete", + "internal", "arguments", "pid"]). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case exchanges0(ReqData) of + vhost_not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + try + Basic = rabbit_mgmt_util:filter_vhost(basic(ReqData), ReqData, + Context), + Data = rabbit_mgmt_util:augment_resources(Basic, ?DEFAULT_SORT, + ?BASIC_COLUMNS, ReqData, + Context, fun augment/2), + rabbit_mgmt_util:reply(Data, ReqData, Context) + catch + {error, invalid_range_parameters, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, Context) + end. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_vhost(ReqData, Context). + +%%-------------------------------------------------------------------- + +augment(Basic, ReqData) -> + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + rabbit_mgmt_db:augment_exchanges(Basic, rabbit_mgmt_util:range(ReqData), + basic); + true -> + Basic + end. + +augmented(ReqData, Context) -> + rabbit_mgmt_db:augment_exchanges( + rabbit_mgmt_util:filter_vhost(basic(ReqData), ReqData, Context), + rabbit_mgmt_util:range(ReqData), basic). + +basic(ReqData) -> + [rabbit_mgmt_format:exchange(X) || X <- exchanges0(ReqData)]. + +exchanges0(ReqData) -> + rabbit_mgmt_util:all_or_one_vhost(ReqData, fun rabbit_exchange:info_all/1). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_extensions.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_extensions.erl new file mode 100644 index 0000000000..26c3ccbee6 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_extensions.erl @@ -0,0 +1,33 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2011-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_extensions). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +to_json(ReqData, Context) -> + Modules = rabbit_mgmt_dispatcher:modules([]), + rabbit_mgmt_util:reply( + [Module:web_ui() || Module <- Modules], ReqData, Context). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_feature_flag_enable.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_feature_flag_enable.erl new file mode 100644 index 0000000000..fafb993951 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_feature_flag_enable.erl @@ -0,0 +1,55 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2019-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_feature_flag_enable). + +-export([init/2, + content_types_accepted/2, is_authorized/2, + allowed_methods/2, accept_content/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _Args) -> + {cowboy_rest, + rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), + #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_accepted(ReqData, Context) -> + {[{{<<"application">>, <<"json">>, '*'}, accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"PUT">>, <<"OPTIONS">>], ReqData, Context}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). + +accept_content(ReqData, #context{} = Context) -> + NameS = rabbit_mgmt_util:id(name, ReqData), + try + Name = list_to_existing_atom(binary_to_list(NameS)), + case rabbit_feature_flags:enable(Name) of + ok -> + {true, ReqData, Context}; + {error, Reason1} -> + FormattedReason1 = rabbit_ff_extra:format_error(Reason1), + rabbit_mgmt_util:bad_request( + list_to_binary(FormattedReason1), ReqData, Context) + end + catch + _:badarg -> + Reason2 = unsupported, + FormattedReason2 = rabbit_ff_extra:format_error(Reason2), + rabbit_mgmt_util:bad_request( + list_to_binary(FormattedReason2), ReqData, Context) + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_feature_flags.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_feature_flags.erl new file mode 100644 index 0000000000..3f25045a36 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_feature_flags.erl @@ -0,0 +1,45 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2019-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_feature_flags). + +-export([init/2, to_json/2, + content_types_provided/2, + is_authorized/2, allowed_methods/2]). +-export([variances/2]). +-export([feature_flags/0]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _Args) -> + {cowboy_rest, + rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), + #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"OPTIONS">>], ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply_list(feature_flags(), ReqData, Context). + +is_authorized(ReqData, Context) -> + {Res, Req2, Context2} = rabbit_mgmt_util:is_authorized_admin(ReqData, Context), + {Res, Req2, Context2}. + +%%-------------------------------------------------------------------- + +feature_flags() -> + rabbit_ff_extra:cli_info(). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_global_parameter.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_global_parameter.erl new file mode 100644 index 0000000000..a81c5be8fa --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_global_parameter.erl @@ -0,0 +1,71 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_global_parameter). + +-export([init/2, resource_exists/2, to_json/2, + content_types_provided/2, content_types_accepted/2, + is_authorized/2, allowed_methods/2, accept_content/2, + delete_resource/2]). +-export([variances/2]). + +-import(rabbit_misc, [pget/2]). + +-include_lib("rabbit_common/include/rabbit.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case parameter(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply(rabbit_mgmt_format:parameter(parameter(ReqData)), + ReqData, Context). + +accept_content(ReqData0, Context = #context{user = #user{username = Username}}) -> + rabbit_mgmt_util:with_decode( + [value], ReqData0, Context, + fun([Value], _, ReqData) -> + Val = if is_map(Value) -> maps:to_list(Value); + true -> Value + end, + rabbit_runtime_parameters:set_global(name(ReqData), Val, Username), + {true, ReqData, Context} + end). + +delete_resource(ReqData, Context = #context{user = #user{username = Username}}) -> + ok = rabbit_runtime_parameters:clear_global(name(ReqData), Username), + {true, ReqData, Context}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_global_parameters(ReqData, Context). + +%%-------------------------------------------------------------------- + +parameter(ReqData) -> + rabbit_runtime_parameters:lookup_global(name(ReqData)). + +name(ReqData) -> rabbit_data_coercion:to_atom(rabbit_mgmt_util:id(name, ReqData)). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_global_parameters.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_global_parameters.erl new file mode 100644 index 0000000000..fa23283ad5 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_global_parameters.erl @@ -0,0 +1,36 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_global_parameters). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([variances/2]). +-export([basic/0]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply_list(basic(), ReqData, Context). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_global_parameters(ReqData, Context). + +%%-------------------------------------------------------------------- + +basic() -> + rabbit_runtime_parameters:list_global(). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_alarms.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_alarms.erl new file mode 100644 index 0000000000..06db3bd79e --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_alarms.erl @@ -0,0 +1,56 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +%% An HTTP API counterpart of 'rabbitmq-diagnostics check_alarms' +-module(rabbit_mgmt_wm_health_check_alarms). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([resource_exists/2]). +-export([variances/2]). + +-include("rabbit_mgmt.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {true, ReqData, Context}. + +to_json(ReqData, Context) -> + Timeout = case cowboy_req:header(<<"timeout">>, ReqData) of + undefined -> 70000; + Val -> list_to_integer(binary_to_list(Val)) + end, + case rabbit_alarm:get_alarms(Timeout) of + [] -> + rabbit_mgmt_util:reply([{status, ok}], ReqData, Context); + Xs when length(Xs) > 0 -> + Msg = "There are alarms in effect in the cluster", + failure(Msg, Xs, ReqData, Context) + end. + +failure(Message, Alarms0, ReqData, Context) -> + Alarms = rabbit_alarm:format_as_maps(Alarms0), + Body = #{ + status => failed, + reason => rabbit_data_coercion:to_binary(Message), + alarms => Alarms + }, + {Response, ReqData1, Context1} = rabbit_mgmt_util:reply(Body, ReqData, Context), + {stop, cowboy_req:reply(?HEALTH_CHECK_FAILURE_STATUS, #{}, Response, ReqData1), Context1}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_certificate_expiration.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_certificate_expiration.erl new file mode 100644 index 0000000000..f0fd466b2a --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_certificate_expiration.erl @@ -0,0 +1,178 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +%% An HTTP API counterpart of 'rabbitmq-diagnostics check_certificate_expiration' +-module(rabbit_mgmt_wm_health_check_certificate_expiration). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([resource_exists/2]). +-export([variances/2]). + +-include_lib("public_key/include/public_key.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +-define(DAYS_SECONDS, 86400). +-define(WEEKS_SECONDS, ?DAYS_SECONDS * 7). +-define(MONTHS_SECONDS, ?DAYS_SECONDS * (365 / 12)). +-define(YEARS_SECONDS, ?DAYS_SECONDS * 365). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {true, ReqData, Context}. + +to_json(ReqData, Context) -> + Listeners = rabbit_networking:active_listeners(), + Local = [L || #listener{node = N} = L <- Listeners, N == node()], + case convert(within(ReqData), unit(ReqData)) of + {error, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, Context); + Seconds -> + ExpiringListeners = lists:foldl(fun(L, Acc) -> + case listener_expiring_within(L, Seconds) of + false -> Acc; + Map -> [Map | Acc] + end + end, [], Local), + case ExpiringListeners of + [] -> + rabbit_mgmt_util:reply([{status, ok}], ReqData, Context); + _ -> + Msg = <<"Certificates expiring">>, + failure(Msg, ExpiringListeners, ReqData, Context) + end + end. + +failure(Message, Listeners, ReqData, Context) -> + {Response, ReqData1, Context1} = rabbit_mgmt_util:reply([{status, failed}, + {reason, Message}, + {expired, Listeners}], + ReqData, Context), + {stop, cowboy_req:reply(503, #{}, Response, ReqData1), Context1}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). + +within(ReqData) -> + rabbit_mgmt_util:id(within, ReqData). + +unit(ReqData) -> + rabbit_mgmt_util:id(unit, ReqData). + +convert(Time, Unit) -> + try + do_convert(binary_to_integer(Time), string:lowercase(binary_to_list(Unit))) + catch + error:badarg -> + {error, "Invalid expiration value."}; + invalid_unit -> + {error, "Time unit not recognised. Use: days, seconds, months, years."} + end. + +do_convert(Time, "days") -> + Time * ?DAYS_SECONDS; +do_convert(Time, "weeks") -> + Time * ?WEEKS_SECONDS; +do_convert(Time, "months") -> + Time * ?MONTHS_SECONDS; +do_convert(Time, "years") -> + Time * ?YEARS_SECONDS; +do_convert(_, _) -> + throw(invalid_unit). + +listener_expiring_within(#listener{node = Node, protocol = Protocol, ip_address = Interface, + port = Port, opts = Opts}, Seconds) -> + Certfile = proplists:get_value(certfile, Opts), + Cacertfile = proplists:get_value(cacertfile, Opts), + Now = calendar:datetime_to_gregorian_seconds(calendar:universal_time()), + ExpiryDate = Now + Seconds, + CertfileExpiresOn = expired(cert_validity(read_cert(Certfile)), ExpiryDate), + CacertfileExpiresOn = expired(cert_validity(read_cert(Cacertfile)), ExpiryDate), + case {CertfileExpiresOn, CacertfileExpiresOn} of + {[], []} -> + false; + _ -> + #{node => Node, + protocol => Protocol, + interface => list_to_binary(inet:ntoa(Interface)), + port => Port, + certfile => list_to_binary(Certfile), + cacertfile => list_to_binary(Cacertfile), + certfile_expires_on => expires_on_list(CertfileExpiresOn), + cacertfile_expires_on => expires_on_list(CacertfileExpiresOn) + } + end. + +expires_on_list({error, Reason}) -> + {error, list_to_binary(Reason)}; +expires_on_list(ExpiresOn) -> + [seconds_to_bin(S) || S <- ExpiresOn]. + +read_cert(undefined) -> + undefined; +read_cert({pem, Pem}) -> + Pem; +read_cert(Path) -> + case file:read_file(Path) of + {ok, Bin} -> + Bin; + Err -> + Err + end. + +cert_validity(undefined) -> + undefined; +cert_validity(Cert) -> + DsaEntries = public_key:pem_decode(Cert), + case DsaEntries of + [] -> + {error, "The certificate file provided does not contain any PEM entry."}; + _ -> + Now = calendar:datetime_to_gregorian_seconds(calendar:universal_time()), + lists:map( + fun({'Certificate', _, _} = DsaEntry) -> + #'Certificate'{tbsCertificate = TBSCertificate} = public_key:pem_entry_decode(DsaEntry), + #'TBSCertificate'{validity = Validity} = TBSCertificate, + #'Validity'{notAfter = NotAfter, notBefore = NotBefore} = Validity, + Start = pubkey_cert:time_str_2_gregorian_sec(NotBefore), + case Start > Now of + true -> + {error, "Certificate is not yet valid"}; + false -> + pubkey_cert:time_str_2_gregorian_sec(NotAfter) + end; + ({Type, _, _}) -> + {error, io_lib:format("The certificate file provided contains a ~p entry", + [Type])} + end, DsaEntries) + end. + +expired(undefined, _ExpiryDate) -> + []; +expired({error, _} = Error, _ExpiryDate) -> + Error; +expired(Expires, ExpiryDate) -> + lists:filter(fun({error, _}) -> + true; + (Seconds) -> + Seconds < ExpiryDate + end, Expires). + +seconds_to_bin(Seconds) -> + {{Y, M, D}, {H, Min, S}} = calendar:gregorian_seconds_to_datetime(Seconds), + list_to_binary(lists:flatten(io_lib:format("~w-~2.2.0w-~2.2.0w ~w:~2.2.0w:~2.2.0w", + [Y, M, D, H, Min, S]))). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_local_alarms.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_local_alarms.erl new file mode 100644 index 0000000000..4553efa3e2 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_local_alarms.erl @@ -0,0 +1,56 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +%% An HTTP API counterpart of 'rabbitmq-dignoastics check_local_alarms' +-module(rabbit_mgmt_wm_health_check_local_alarms). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([resource_exists/2]). +-export([variances/2]). + +-include("rabbit_mgmt.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {true, ReqData, Context}. + +to_json(ReqData, Context) -> + Timeout = case cowboy_req:header(<<"timeout">>, ReqData) of + undefined -> 70000; + Val -> list_to_integer(binary_to_list(Val)) + end, + case rabbit_alarm:get_local_alarms(Timeout) of + [] -> + rabbit_mgmt_util:reply([{status, ok}], ReqData, Context); + Xs when length(Xs) > 0 -> + Msg = "There are alarms in effect on the node", + failure(Msg, Xs, ReqData, Context) + end. + +failure(Message, Alarms0, ReqData, Context) -> + Alarms = rabbit_alarm:format_as_maps(Alarms0), + Body = #{ + status => failed, + reason => rabbit_data_coercion:to_binary(Message), + alarms => Alarms + }, + {Response, ReqData1, Context1} = rabbit_mgmt_util:reply(Body, ReqData, Context), + {stop, cowboy_req:reply(?HEALTH_CHECK_FAILURE_STATUS, #{}, Response, ReqData1), Context1}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_node_is_mirror_sync_critical.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_node_is_mirror_sync_critical.erl new file mode 100644 index 0000000000..cef5512551 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_node_is_mirror_sync_critical.erl @@ -0,0 +1,54 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +%% An HTTP API counterpart of 'rabbitmq-diagnostics check_if_node_is_quorum_critical' +-module(rabbit_mgmt_wm_health_check_node_is_mirror_sync_critical). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([resource_exists/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {true, ReqData, Context}. + +to_json(ReqData, Context) -> + case rabbit_nodes:is_single_node_cluster() of + true -> + rabbit_mgmt_util:reply([{status, ok}, + {reason, <<"single node cluster">>}], ReqData, Context); + false -> + case rabbit_amqqueue:list_local_mirrored_classic_without_synchronised_mirrors_for_cli() of + [] -> + rabbit_mgmt_util:reply([{status, ok}], ReqData, Context); + Qs when length(Qs) > 0 -> + Msg = <<"There are classic mirrored queues without online synchronised mirrors">>, + failure(Msg, Qs, ReqData, Context) + end + end. + +failure(Message, Qs, ReqData, Context) -> + {Response, ReqData1, Context1} = rabbit_mgmt_util:reply([{status, failed}, + {reason, Message}, + {queues, Qs}], + ReqData, Context), + {stop, cowboy_req:reply(503, #{}, Response, ReqData1), Context1}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_node_is_quorum_critical.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_node_is_quorum_critical.erl new file mode 100644 index 0000000000..857c72b30b --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_node_is_quorum_critical.erl @@ -0,0 +1,54 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +%% An HTTP API counterpart of 'rabbitmq-diagnostics check_if_node_is_quorum_critical' +-module(rabbit_mgmt_wm_health_check_node_is_quorum_critical). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([resource_exists/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {true, ReqData, Context}. + +to_json(ReqData, Context) -> + case rabbit_nodes:is_single_node_cluster() of + true -> + rabbit_mgmt_util:reply([{status, ok}, + {reason, <<"single node cluster">>}], ReqData, Context); + false -> + case rabbit_quorum_queue:list_with_minimum_quorum_for_cli() of + [] -> + rabbit_mgmt_util:reply([{status, ok}], ReqData, Context); + Qs when length(Qs) > 0 -> + Msg = <<"There are quorum queues that would lose their quorum if the target node is shut down">>, + failure(Msg, Qs, ReqData, Context) + end + end. + +failure(Message, Qs, ReqData, Context) -> + {Response, ReqData1, Context1} = rabbit_mgmt_util:reply([{status, failed}, + {reason, Message}, + {queues, Qs}], + ReqData, Context), + {stop, cowboy_req:reply(503, #{}, Response, ReqData1), Context1}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_port_listener.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_port_listener.erl new file mode 100644 index 0000000000..1fcc00f613 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_port_listener.erl @@ -0,0 +1,66 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +%% An HTTP API counterpart of 'rabbitmq-diagnostics check_port_listener' +-module(rabbit_mgmt_wm_health_check_port_listener). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([resource_exists/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case port(ReqData) of + none -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + try + Port = binary_to_integer(port(ReqData)), + Listeners = rabbit_networking:active_listeners(), + Local = [L || #listener{node = N} = L <- Listeners, N == node()], + PortListeners = [L || #listener{port = P} = L <- Local, P == Port], + case PortListeners of + [] -> + Msg = <<"No active listener">>, + failure(Msg, Port, [P || #listener{port = P} <- Local], ReqData, Context); + _ -> + rabbit_mgmt_util:reply([{status, ok}, + {port, Port}], ReqData, Context) + end + catch + error:badarg -> + rabbit_mgmt_util:bad_request(<<"Invalid port">>, ReqData, Context) + end. + +failure(Message, Missing, Ports, ReqData, Context) -> + {Response, ReqData1, Context1} = rabbit_mgmt_util:reply([{status, failed}, + {reason, Message}, + {missing, Missing}, + {ports, Ports}], + ReqData, Context), + {stop, cowboy_req:reply(503, #{}, Response, ReqData1), Context1}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). + +port(ReqData) -> + rabbit_mgmt_util:id(port, ReqData). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_protocol_listener.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_protocol_listener.erl new file mode 100644 index 0000000000..a0b7e4e6dd --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_protocol_listener.erl @@ -0,0 +1,127 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +%% An HTTP API counterpart of 'rabbitmq-diagnostics check_protocol_listener' +-module(rabbit_mgmt_wm_health_check_protocol_listener). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([resource_exists/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case protocol(ReqData) of + none -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + Protocol = normalize_protocol(protocol(ReqData)), + Listeners = rabbit_networking:active_listeners(), + Local = [L || #listener{node = N} = L <- Listeners, N == node()], + ProtoListeners = [L || #listener{protocol = P} = L <- Local, atom_to_list(P) == Protocol], + case ProtoListeners of + [] -> + Msg = <<"No active listener">>, + failure(Msg, Protocol, [P || #listener{protocol = P} <- Local], ReqData, Context); + _ -> + rabbit_mgmt_util:reply([{status, ok}, + {protocol, list_to_binary(Protocol)}], ReqData, Context) + end. + +failure(Message, Missing, Protocols, ReqData, Context) -> + {Response, ReqData1, Context1} = rabbit_mgmt_util:reply([{status, failed}, + {reason, Message}, + {missing, list_to_binary(Missing)}, + {protocols, Protocols}], + ReqData, Context), + {stop, cowboy_req:reply(503, #{}, Response, ReqData1), Context1}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). + +protocol(ReqData) -> + rabbit_mgmt_util:id(protocol, ReqData). + +normalize_protocol(Protocol) -> + case string:lowercase(binary_to_list(Protocol)) of + "amqp091" -> "amqp"; + "amqp0.9.1" -> "amqp"; + "amqp0-9-1" -> "amqp"; + "amqp0_9_1" -> "amqp"; + "amqp10" -> "amqp"; + "amqp1.0" -> "amqp"; + "amqp1-0" -> "amqp"; + "amqp1_0" -> "amqp"; + "amqps" -> "amqp/ssl"; + "mqtt3.1" -> "mqtt"; + "mqtt3.1.1" -> "mqtt"; + "mqtt31" -> "mqtt"; + "mqtt311" -> "mqtt"; + "mqtt3_1" -> "mqtt"; + "mqtt3_1_1" -> "mqtt"; + "mqtts" -> "mqtt/ssl"; + "mqtt+tls" -> "mqtt/ssl"; + "mqtt+ssl" -> "mqtt/ssl"; + "stomp1.0" -> "stomp"; + "stomp1.1" -> "stomp"; + "stomp1.2" -> "stomp"; + "stomp10" -> "stomp"; + "stomp11" -> "stomp"; + "stomp12" -> "stomp"; + "stomp1_0" -> "stomp"; + "stomp1_1" -> "stomp"; + "stomp1_2" -> "stomp"; + "stomps" -> "stomp/ssl"; + "stomp+tls" -> "stomp/ssl"; + "stomp+ssl" -> "stomp/ssl"; + "https" -> "https"; + "http1" -> "http"; + "http1.1" -> "http"; + "http_api" -> "http"; + "management" -> "http"; + "management_ui" -> "http"; + "ui" -> "http"; + "cli" -> "clustering"; + "distribution" -> "clustering"; + "webmqtt" -> "http/web-mqtt"; + "web-mqtt" -> "http/web-mqtt"; + "web_mqtt" -> "http/web-mqtt"; + "webmqtt/tls" -> "https/web-mqtt"; + "web-mqtt/tls" -> "https/web-mqtt"; + "webmqtt/ssl" -> "https/web-mqtt"; + "web-mqtt/ssl" -> "https/web-mqtt"; + "webmqtt+tls" -> "https/web-mqtt"; + "web-mqtt+tls" -> "https/web-mqtt"; + "webmqtt+ssl" -> "https/web-mqtt"; + "web-mqtt+ssl" -> "https/web-mqtt"; + "webstomp" -> "http/web-stomp"; + "web-stomp" -> "http/web-stomp"; + "web_stomp" -> "http/web-stomp"; + "webstomp/tls" -> "https/web-stomp"; + "web-stomp/tls" -> "https/web-stomp"; + "webstomp/ssl" -> "https/web-stomp"; + "web-stomp/ssl" -> "https/web-stomp"; + "webstomp+tls" -> "https/web-stomp"; + "web-stomp+tls" -> "https/web-stomp"; + "webstomp+ssl" -> "https/web-stomp"; + "web-stomp+ssl" -> "https/web-stomp"; + Any -> Any + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_virtual_hosts.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_virtual_hosts.erl new file mode 100644 index 0000000000..0bc9680adc --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_health_check_virtual_hosts.erl @@ -0,0 +1,48 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +%% An HTTP API counterpart of 'rabbitmq-diagnostics check_virtual_hosts' +-module(rabbit_mgmt_wm_health_check_virtual_hosts). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([resource_exists/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {true, ReqData, Context}. + +to_json(ReqData, Context) -> + case rabbit_vhost_sup_sup:check() of + [] -> + rabbit_mgmt_util:reply([{status, ok}], ReqData, Context); + Vs when length(Vs) > 0 -> + Msg = <<"Some virtual hosts are down">>, + failure(Msg, Vs, ReqData, Context) + end. + +failure(Message, Vs, ReqData, Context) -> + {Response, ReqData1, Context1} = rabbit_mgmt_util:reply([{status, failed}, + {reason, Message}, + {'virtual-hosts', Vs}], + ReqData, Context), + {stop, cowboy_req:reply(503, #{}, Response, ReqData1), Context1}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_healthchecks.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_healthchecks.erl new file mode 100644 index 0000000000..4fc61bd0a7 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_healthchecks.erl @@ -0,0 +1,76 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +%% This original One True Health Checkâ„¢ has been deprecated as too coarse-grained, +%% intrusive and prone to false positives under load. +-module(rabbit_mgmt_wm_healthchecks). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([resource_exists/2]). +-export([variances/2]). + +-include("rabbit_mgmt.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case node0(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + Node = node0(ReqData), + Timeout = case cowboy_req:header(<<"timeout">>, ReqData) of + undefined -> 70000; + Val -> list_to_integer(binary_to_list(Val)) + end, + case rabbit_health_check:node(Node, Timeout) of + ok -> + rabbit_mgmt_util:reply([{status, ok}], ReqData, Context); + {badrpc, timeout} -> + ErrMsg = rabbit_mgmt_format:print("node ~p health check timed out", [Node]), + failure(ErrMsg, ReqData, Context); + {badrpc, Err} -> + failure(rabbit_mgmt_format:print("~p", Err), ReqData, Context); + {error_string, Err} -> + S = rabbit_mgmt_format:escape_html_tags( + rabbit_data_coercion:to_list(rabbit_mgmt_format:print(Err))), + failure(S, ReqData, Context) + end. + +failure(Message, ReqData, Context) -> + {Response, ReqData1, Context1} = rabbit_mgmt_util:reply([{status, failed}, + {reason, Message}], + ReqData, Context), + {stop, cowboy_req:reply(?HEALTH_CHECK_FAILURE_STATUS, #{}, Response, ReqData1), Context1}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). + +node0(ReqData) -> + Node = case rabbit_mgmt_util:id(node, ReqData) of + none -> + node(); + Node0 -> + list_to_atom(binary_to_list(Node0)) + end, + case [N || N <- rabbit_mgmt_wm_nodes:all_nodes(ReqData), + proplists:get_value(name, N) == Node] of + [] -> not_found; + [_] -> Node + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_limit.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_limit.erl new file mode 100644 index 0000000000..5f0e27c4ba --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_limit.erl @@ -0,0 +1,64 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_limit). + +-export([init/2, + content_types_accepted/2, is_authorized/2, + allowed_methods/2, accept_content/2, + delete_resource/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_accepted(ReqData, Context) -> + {[{{<<"application">>, <<"json">>, '*'}, accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +accept_content(ReqData0, Context = #context{user = #user{username = Username}}) -> + case rabbit_mgmt_util:vhost(ReqData0) of + not_found -> + rabbit_mgmt_util:not_found(vhost_not_found, ReqData0, Context); + VHost -> + rabbit_mgmt_util:with_decode( + [value], ReqData0, Context, + fun([Value], _Body, ReqData) -> + Name = rabbit_mgmt_util:id(name, ReqData), + case rabbit_vhost_limit:update_limit(VHost, Name, Value, + Username) of + ok -> + {true, ReqData, Context}; + {error_string, Reason} -> + rabbit_mgmt_util:bad_request( + list_to_binary(Reason), ReqData, Context) + end + end) + end. + +delete_resource(ReqData, Context = #context{user = #user{username = Username}}) -> + ok = rabbit_vhost_limit:clear_limit(rabbit_mgmt_util:vhost(ReqData), + name(ReqData), Username), + {true, ReqData, Context}. + + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). + +%%-------------------------------------------------------------------- + +name(ReqData) -> rabbit_mgmt_util:id(name, ReqData). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_limits.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_limits.erl new file mode 100644 index 0000000000..96fbf64100 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_limits.erl @@ -0,0 +1,59 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_limits). + +-export([init/2, to_json/2, content_types_provided/2, + resource_exists/2, is_authorized/2, allowed_methods/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"GET">>, <<"OPTIONS">>], ReqData, Context}. + +%% Admin user can see all vhosts + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_vhost_visible(ReqData, Context). + +content_types_provided(ReqData, Context) -> + {[{<<"application/json">>, to_json}], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case rabbit_mgmt_util:vhost(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply_list(limits(ReqData, Context), [], ReqData, Context). + +limits(ReqData, Context) -> + case rabbit_mgmt_util:vhost(ReqData) of + none -> + User = Context#context.user, + VisibleVhosts = rabbit_mgmt_util:list_visible_vhosts_names(User), + [ [{vhost, VHost}, {value, Value}] + || {VHost, Value} <- rabbit_vhost_limit:list(), + lists:member(VHost, VisibleVhosts) ]; + VHost when is_binary(VHost) -> + case rabbit_vhost_limit:list(VHost) of + [] -> []; + Value -> [[{vhost, VHost}, {value, Value}]] + end + end. +%%-------------------------------------------------------------------- diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_login.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_login.erl new file mode 100644 index 0000000000..fec7c99643 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_login.erl @@ -0,0 +1,53 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2011-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_login). + +-export([init/2, is_authorized/2, + allowed_methods/2, accept_content/2, content_types_provided/2, + content_types_accepted/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"POST">>, <<"OPTIONS">>], ReqData, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{{<<"application">>, <<"x-www-form-urlencoded">>, '*'}, accept_content}], ReqData, Context}. + +is_authorized(#{method := <<"OPTIONS">>} = ReqData, Context) -> + {true, ReqData, Context}; +is_authorized(ReqData0, Context) -> + {ok, Body, ReqData} = cowboy_req:read_urlencoded_body(ReqData0), + Username = proplists:get_value(<<"username">>, Body), + Password = proplists:get_value(<<"password">>, Body), + case rabbit_mgmt_util:is_authorized_user(ReqData, Context, Username, Password) of + {true, ReqData1, Context1} -> + Value = base64:encode(<<Username/binary,":",Password/binary>>), + {true, cowboy_req:set_resp_cookie(<<"auth">>, Value, ReqData1), Context1}; + Other -> + Other + end. + +accept_content(ReqData, Context) -> + rabbit_mgmt_util:post_respond(do_login(ReqData, Context)). + +do_login(ReqData, Context) -> + rabbit_mgmt_util:reply(ok, ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_node.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_node.erl new file mode 100644 index 0000000000..52c1d4d9bb --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_node.erl @@ -0,0 +1,78 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_node). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([resource_exists/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case node0(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply(node0(ReqData), ReqData, Context). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_monitor(ReqData, Context). + +%%-------------------------------------------------------------------- + +node0(ReqData) -> + Node = list_to_atom(binary_to_list(rabbit_mgmt_util:id(node, ReqData))), + [Data] = node_data(Node, ReqData), + augment(ReqData, Node, Data). + +augment(ReqData, Node, Data) -> + lists:foldl(fun (Key, DataN) -> augment(Key, ReqData, Node, DataN) end, + Data, [memory, binary]). + +augment(Key, ReqData, Node, Data) -> + case rabbit_mgmt_util:qs_val(list_to_binary(atom_to_list(Key)), ReqData) of + <<"true">> -> Res = case rpc:call(Node, rabbit_vm, Key, [], infinity) of + {badrpc, _} -> not_available; + Result -> Result + end, + [{Key, Res} | Data]; + _ -> Data + end. + +node_data(Node, ReqData) -> + S = rabbit_mnesia:status(), + Nodes = proplists:get_value(nodes, S), + Running = proplists:get_value(running_nodes, S), + Type = find_type(Node, Nodes), + Basic = [[{name, Node}, {running, lists:member(Node, Running)}, {type, Type}]], + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + rabbit_mgmt_db:augment_nodes(Basic, rabbit_mgmt_util:range_ceil(ReqData)); + true -> + Basic + end. + +find_type(Node, [{Type, Nodes} | Rest]) -> + case lists:member(Node, Nodes) of + true -> Type; + false -> find_type(Node, Rest) + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_node_memory.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_node_memory.erl new file mode 100644 index 0000000000..19e4237b44 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_node_memory.erl @@ -0,0 +1,75 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_node_memory). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([resource_exists/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, [Mode]) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), {Mode, #context{}}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {node_exists(ReqData, get_node(ReqData)), ReqData, Context}. + +to_json(ReqData, {Mode, Context}) -> + rabbit_mgmt_util:reply(augment(Mode, ReqData), ReqData, {Mode, Context}). + +is_authorized(ReqData, {Mode, Context}) -> + {Res, RD, C} = rabbit_mgmt_util:is_authorized_monitor(ReqData, Context), + {Res, RD, {Mode, C}}. + +%%-------------------------------------------------------------------- +get_node(ReqData) -> + list_to_atom(binary_to_list(rabbit_mgmt_util:id(node, ReqData))). + +node_exists(ReqData, Node) -> + case [N || N <- rabbit_mgmt_wm_nodes:all_nodes(ReqData), + proplists:get_value(name, N) == Node] of + [] -> false; + [_] -> true + end. + +augment(Mode, ReqData) -> + Node = get_node(ReqData), + case node_exists(ReqData, Node) of + false -> + not_found; + true -> + case rpc:call(Node, rabbit_vm, memory, [], infinity) of + {badrpc, _} -> [{memory, not_available}]; + Result -> [{memory, format(Mode, Result)}] + end + end. + +format(absolute, Result) -> + Result; +format(relative, Result) -> + {value, {total, Totals}, Rest} = lists:keytake(total, 1, Result), + Total = proplists:get_value(rss, Totals), + [{total, 100} | [{K, percentage(V, Total)} || {K, V} <- Rest, + K =/= strategy]]. + +percentage(Part, Total) -> + case round((Part/Total) * 100) of + 0 when Part =/= 0 -> + 1; + Int -> + Int + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_node_memory_ets.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_node_memory_ets.erl new file mode 100644 index 0000000000..2bc40d22ce --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_node_memory_ets.erl @@ -0,0 +1,85 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_node_memory_ets). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([resource_exists/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, [Mode]) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), {Mode, #context{}}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {node_exists(ReqData, get_node(ReqData)), ReqData, Context}. + +to_json(ReqData, {Mode, Context}) -> + rabbit_mgmt_util:reply(augment(Mode, ReqData), ReqData, {Mode, Context}). + +is_authorized(ReqData, {Mode, Context}) -> + {Res, RD, C} = rabbit_mgmt_util:is_authorized_monitor(ReqData, Context), + {Res, RD, {Mode, C}}. + +%%-------------------------------------------------------------------- +get_node(ReqData) -> + list_to_atom(binary_to_list(rabbit_mgmt_util:id(node, ReqData))). + +get_filter(ReqData) -> + case rabbit_mgmt_util:id(filter, ReqData) of + none -> all; + <<"management">> -> rabbit_mgmt_storage; + Other when is_binary(Other) -> list_to_atom(binary_to_list(Other)); + _ -> all + end. + +node_exists(ReqData, Node) -> + case [N || N <- rabbit_mgmt_wm_nodes:all_nodes(ReqData), + proplists:get_value(name, N) == Node] of + [] -> false; + [_] -> true + end. + +augment(Mode, ReqData) -> + Node = get_node(ReqData), + Filter = get_filter(ReqData), + case node_exists(ReqData, Node) of + false -> + not_found; + true -> + case rpc:call(Node, rabbit_vm, ets_tables_memory, + [Filter], infinity) of + {badrpc, _} -> [{ets_tables_memory, not_available}]; + [] -> [{ets_tables_memory, no_tables}]; + Result -> [{ets_tables_memory, format(Mode, Result)}] + end + end. + +format(absolute, Result) -> + Total = lists:sum([V || {_K,V} <- Result]), + [{total, Total} | Result]; +format(relative, Result) -> + Total = lists:sum([V || {_K,V} <- Result]), + [{total, 100} | [{K, percentage(V, Total)} || {K, V} <- Result]]. + +percentage(Part, Total) -> + case round((Part/Total) * 100) of + 0 when Part =/= 0 -> + 1; + Int -> + Int + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_nodes.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_nodes.erl new file mode 100644 index 0000000000..c3de2e0bd2 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_nodes.erl @@ -0,0 +1,56 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_nodes). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([all_nodes/1, all_nodes_raw/0]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +to_json(ReqData, Context) -> + try + rabbit_mgmt_util:reply_list(all_nodes(ReqData), ReqData, Context) + catch + {error, invalid_range_parameters, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, Context) + end. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_monitor(ReqData, Context). + +%%-------------------------------------------------------------------- + +all_nodes(ReqData) -> + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + rabbit_mgmt_db:augment_nodes( + all_nodes_raw(), rabbit_mgmt_util:range_ceil(ReqData)); + true -> + all_nodes_raw() + end. + +all_nodes_raw() -> + S = rabbit_mnesia:status(), + Nodes = proplists:get_value(nodes, S), + Types = proplists:get_keys(Nodes), + Running = proplists:get_value(running_nodes, S), + [[{name, Node}, {type, Type}, {running, lists:member(Node, Running)}] || + Type <- Types, Node <- proplists:get_value(Type, Nodes)]. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_operator_policies.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_operator_policies.erl new file mode 100644 index 0000000000..7490c427dd --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_operator_policies.erl @@ -0,0 +1,49 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_operator_policies). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2, + resource_exists/2, basic/1]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {[{<<"application/json">>, to_json}], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case basic(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply_list( + rabbit_mgmt_util:filter_vhost(basic(ReqData), ReqData, Context), + ["priority"], ReqData, Context). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_vhost(ReqData, Context). + +%%-------------------------------------------------------------------- + +basic(ReqData) -> + case rabbit_mgmt_util:vhost(ReqData) of + not_found -> not_found; + none -> rabbit_policy:list_op(); + VHost -> rabbit_policy:list_op(VHost) + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_operator_policy.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_operator_policy.erl new file mode 100644 index 0000000000..85bec3631d --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_operator_policy.erl @@ -0,0 +1,83 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_operator_policy). + +-export([init/2, resource_exists/2, to_json/2, + content_types_provided/2, content_types_accepted/2, + is_authorized/2, allowed_methods/2, accept_content/2, + delete_resource/2]). +-export([variances/2]). + + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {[{<<"application/json">>, to_json}], ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{{<<"application">>, <<"json">>, '*'}, accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case policy(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply(policy(ReqData), ReqData, Context). + +accept_content(ReqData0, Context = #context{user = #user{username = Username}}) -> + case rabbit_mgmt_util:vhost(ReqData0) of + not_found -> + rabbit_mgmt_util:not_found(vhost_not_found, ReqData0, Context); + VHost -> + rabbit_mgmt_util:with_decode( + [pattern, definition], ReqData0, Context, + fun([Pattern, Definition], Body, ReqData) -> + case rabbit_policy:set_op( + VHost, name(ReqData), Pattern, + maps:to_list(Definition), + maps:get(priority, Body, undefined), + maps:get('apply-to', Body, undefined), + Username) of + ok -> + {true, ReqData, Context}; + {error_string, Reason} -> + rabbit_mgmt_util:bad_request( + list_to_binary(Reason), ReqData, Context) + end + end) + end. + +delete_resource(ReqData, Context = #context{user = #user{username = Username}}) -> + ok = rabbit_policy:delete_op( + rabbit_mgmt_util:vhost(ReqData), name(ReqData), Username), + {true, ReqData, Context}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). + +%%-------------------------------------------------------------------- + +policy(ReqData) -> + rabbit_policy:lookup_op( + rabbit_mgmt_util:vhost(ReqData), name(ReqData)). + +name(ReqData) -> rabbit_mgmt_util:id(name, ReqData). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_overview.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_overview.erl new file mode 100644 index 0000000000..3a7e821b7d --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_overview.erl @@ -0,0 +1,182 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_overview). + +-export([init/2]). +-export([to_json/2, content_types_provided/2, is_authorized/2]). +-export([variances/2]). + +-import(rabbit_misc, [pget/2, pget/3]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +to_json(ReqData, Context = #context{user = User = #user{tags = Tags}}) -> + RatesMode = rabbit_mgmt_agent_config:get_env(rates_mode), + SRP = get_sample_retention_policies(), + %% NB: this duplicates what's in /nodes but we want a global idea + %% of this. And /nodes is not accessible to non-monitor users. + ExchangeTypes = lists:sort( + fun(ET1, ET2) -> + proplists:get_value(name, ET1, none) + =< + proplists:get_value(name, ET2, none) + end, + rabbit_mgmt_external_stats:list_registry_plugins(exchange)), + Overview0 = [{management_version, version(rabbitmq_management)}, + {rates_mode, RatesMode}, + {sample_retention_policies, SRP}, + {exchange_types, ExchangeTypes}, + {product_version, list_to_binary(rabbit:product_version())}, + {product_name, list_to_binary(rabbit:product_name())}, + {rabbitmq_version, list_to_binary(rabbit:base_product_version())}, + {cluster_name, rabbit_nodes:cluster_name()}, + {erlang_version, erlang_version()}, + {erlang_full_version, erlang_full_version()}, + {disable_stats, rabbit_mgmt_util:disable_stats(ReqData)}, + {enable_queue_totals, rabbit_mgmt_util:enable_queue_totals(ReqData)}], + try + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + Range = rabbit_mgmt_util:range(ReqData), + Overview = + case rabbit_mgmt_util:is_monitor(Tags) of + true -> + Overview0 ++ + [{K, maybe_map(V)} || + {K,V} <- rabbit_mgmt_db:get_overview(Range)] ++ + [{node, node()}, + {listeners, listeners()}, + {contexts, web_contexts(ReqData)}]; + _ -> + Overview0 ++ + [{K, maybe_map(V)} || + {K, V} <- rabbit_mgmt_db:get_overview(User, Range)] + end, + rabbit_mgmt_util:reply(Overview, ReqData, Context); + true -> + VHosts = case rabbit_mgmt_util:is_monitor(Tags) of + true -> rabbit_vhost:list_names(); + _ -> rabbit_mgmt_util:list_visible_vhosts_names(User) + end, + + ObjectTotals = case rabbit_mgmt_util:is_monitor(Tags) of + true -> + [{queues, rabbit_amqqueue:count()}, + {exchanges, rabbit_exchange:count()}, + {connections, rabbit_connection_tracking:count()}]; + _ -> + [{queues, length([Q || V <- VHosts, Q <- rabbit_amqqueue:list(V)])}, + {exchanges, length([X || V <- VHosts, X <- rabbit_exchange:list(V)])}] + end, + Overview = Overview0 ++ + [{node, node()}, + {object_totals, ObjectTotals}], + rabbit_mgmt_util:reply(Overview, ReqData, Context) + end + catch + {error, invalid_range_parameters, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, Context) + end. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). + +%%-------------------------------------------------------------------- + +version(App) -> + {ok, V} = application:get_key(App, vsn), + list_to_binary(V). + +listeners() -> + rabbit_mgmt_util:sort_list( + [rabbit_mgmt_format:listener(L) + || L <- rabbit_networking:active_listeners()], + ["protocol", "port", "node"] ). + +maybe_map(L) when is_list(L) -> maps:from_list(L); +maybe_map(V) -> V. + +%%-------------------------------------------------------------------- + +web_contexts(ReqData) -> + rabbit_mgmt_util:sort_list( + lists:append( + [fmt_contexts(N) || N <- rabbit_mgmt_wm_nodes:all_nodes(ReqData)]), + ["description", "port", "node"]). + +fmt_contexts(Node) -> + [fmt_context(Node, C) || C <- pget(contexts, Node, [])]. + +fmt_context(Node, C) -> + rabbit_mgmt_format:web_context([{node, pget(name, Node)} | C]). + +erlang_version() -> list_to_binary(rabbit_misc:otp_release()). + +erlang_full_version() -> + list_to_binary(rabbit_misc:otp_system_version()). + +get_sample_retention_policies() -> + P = rabbit_mgmt_agent_config:get_env(sample_retention_policies), + get_sample_retention_policies(P). + +get_sample_retention_policies(undefined) -> + [{global, []}, {basic, []}, {detailed, []}]; +get_sample_retention_policies(Policies) -> + [transform_retention_policy(Pol, Policies) || Pol <- [global, basic, detailed]]. + +transform_retention_policy(Pol, Policies) -> + case proplists:lookup(Pol, Policies) of + none -> + {Pol, []}; + {Pol, Intervals} -> + {Pol, transform_retention_intervals(Intervals, [])} + end. + +transform_retention_intervals([], Acc) -> + lists:sort(Acc); +transform_retention_intervals([{MaxAgeInSeconds, _}|Rest], Acc) -> + % + % Seconds | Interval + % 60 | last minute + % 600 | last 10 minutes + % 3600 | last hour + % 28800 | last 8 hours + % 86400 | last day + % + % rabbitmq/rabbitmq-management#635 + % + % We check for the max age in seconds to be within 10% of the value above. + % The reason being that the default values are "bit higher" to accommodate + % edge cases (see deps/rabbitmq_management_agent/Makefile) + AccVal = if + MaxAgeInSeconds >= 0 andalso MaxAgeInSeconds =< 66 -> + 60; + MaxAgeInSeconds >= 540 andalso MaxAgeInSeconds =< 660 -> + 600; + MaxAgeInSeconds >= 3240 andalso MaxAgeInSeconds =< 3960 -> + 3600; + MaxAgeInSeconds >= 25920 andalso MaxAgeInSeconds =< 31681 -> + 28800; + MaxAgeInSeconds >= 77760 andalso MaxAgeInSeconds =< 95041 -> + 86400; + true -> + 0 + end, + transform_retention_intervals(Rest, [AccVal|Acc]). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_parameter.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_parameter.erl new file mode 100644 index 0000000000..17a97ca4bf --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_parameter.erl @@ -0,0 +1,88 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_parameter). + +-export([init/2, resource_exists/2, to_json/2, + content_types_provided/2, content_types_accepted/2, + is_authorized/2, allowed_methods/2, accept_content/2, + delete_resource/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case parameter(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply(rabbit_mgmt_format:parameter( + rabbit_mgmt_wm_parameters:fix_shovel_publish_properties(parameter(ReqData))), + ReqData, Context). + +accept_content(ReqData0, Context = #context{user = User}) -> + case rabbit_mgmt_util:vhost(ReqData0) of + not_found -> + rabbit_mgmt_util:not_found(vhost_not_found, ReqData0, Context); + VHost -> + rabbit_mgmt_util:with_decode( + [value], ReqData0, Context, + fun([Value], _, ReqData) -> + case rabbit_runtime_parameters:set( + VHost, component(ReqData), name(ReqData), + if + is_map(Value) -> maps:to_list(Value); + true -> Value + end, + User) of + ok -> + {true, ReqData, Context}; + {error_string, Reason} -> + S = rabbit_mgmt_format:escape_html_tags( + rabbit_data_coercion:to_list(Reason)), + rabbit_mgmt_util:bad_request( + rabbit_data_coercion:to_binary(S), ReqData, Context) + end + end) + end. + +delete_resource(ReqData, Context = #context{user = #user{username = Username}}) -> + ok = rabbit_runtime_parameters:clear( + rabbit_mgmt_util:vhost(ReqData), component(ReqData), name(ReqData), Username), + {true, ReqData, Context}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_policies(ReqData, Context). + +%%-------------------------------------------------------------------- + +parameter(ReqData) -> + rabbit_runtime_parameters:lookup( + rabbit_mgmt_util:vhost(ReqData), component(ReqData), name(ReqData)). + +component(ReqData) -> rabbit_mgmt_util:id(component, ReqData). +name(ReqData) -> rabbit_mgmt_util:id(name, ReqData). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_parameters.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_parameters.erl new file mode 100644 index 0000000000..6acd7a608f --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_parameters.erl @@ -0,0 +1,78 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_parameters). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2, + resource_exists/2, basic/1]). +-export([fix_shovel_publish_properties/1]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case basic(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply_list( + rabbit_mgmt_util:filter_vhost(basic(ReqData), ReqData, Context), + ReqData, Context). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_policies(ReqData, Context). + +%%-------------------------------------------------------------------- + +%% Hackish fix to make sure we return a JSON object instead of an empty list +%% when the publish-properties value is empty. Should be removed in 3.7.0 +%% when we switch to a new JSON library. +fix_shovel_publish_properties(P) -> + case lists:keyfind(component, 1, P) of + {_, <<"shovel">>} -> + case lists:keytake(value, 1, P) of + {value, {_, Values}, P2} -> + case lists:keytake(<<"publish-properties">>, 1, Values) of + {_, {_, []}, Values2} -> + P2 ++ [{value, Values2 ++ [{<<"publish-properties">>, empty_struct}]}]; + _ -> + P + end; + _ -> P + end; + _ -> P + end. + +basic(ReqData) -> + Raw = case rabbit_mgmt_util:id(component, ReqData) of + none -> rabbit_runtime_parameters:list(); + Name -> case rabbit_mgmt_util:vhost(ReqData) of + none -> rabbit_runtime_parameters:list_component( + Name); + not_found -> not_found; + VHost -> rabbit_runtime_parameters:list( + VHost, Name) + end + end, + case Raw of + not_found -> not_found; + _ -> [rabbit_mgmt_format:parameter(fix_shovel_publish_properties(P)) || P <- Raw] + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_permission.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_permission.erl new file mode 100644 index 0000000000..74df5ab960 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_permission.erl @@ -0,0 +1,102 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_permission). + +-export([init/2, resource_exists/2, to_json/2, + content_types_provided/2, content_types_accepted/2, + is_authorized/2, allowed_methods/2, accept_content/2, + delete_resource/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case perms(ReqData) of + none -> false; + not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply(perms(ReqData), ReqData, Context). + +accept_content(ReqData0, Context = #context{user = #user{username = Username}}) -> + case perms(ReqData0) of + not_found -> + rabbit_mgmt_util:bad_request(vhost_or_user_not_found, + ReqData0, Context); + _ -> + User = rabbit_mgmt_util:id(user, ReqData0), + VHost = rabbit_mgmt_util:id(vhost, ReqData0), + rabbit_mgmt_util:with_decode( + [configure, write, read], ReqData0, Context, + fun([Conf, Write, Read], _, ReqData) -> + rabbit_auth_backend_internal:set_permissions( + User, VHost, Conf, Write, Read, Username), + {true, ReqData, Context} + end) + end. + +delete_resource(ReqData, Context = #context{user = #user{username = Username}}) -> + User = rabbit_mgmt_util:id(user, ReqData), + VHost = rabbit_mgmt_util:id(vhost, ReqData), + rabbit_auth_backend_internal:clear_permissions(User, VHost, Username), + {true, ReqData, Context}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). + +%%-------------------------------------------------------------------- + +perms(ReqData) -> + User = rabbit_mgmt_util:id(user, ReqData), + case rabbit_auth_backend_internal:lookup_user(User) of + {ok, _} -> + case rabbit_mgmt_util:vhost(ReqData) of + not_found -> + not_found; + VHost -> + rabbit_mgmt_util:catch_no_such_user_or_vhost( + fun() -> + Perms = + rabbit_auth_backend_internal:list_user_vhost_permissions( + User, VHost), + case Perms of + [Rest] -> [{user, User}, + {vhost, VHost} | Rest]; + [] -> none + end + end, + fun() -> not_found end) + end; + {error, _} -> + not_found + end. + + + + + diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_permissions.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_permissions.erl new file mode 100644 index 0000000000..ad1799b84b --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_permissions.erl @@ -0,0 +1,38 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_permissions). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([permissions/0]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply_list(permissions(), ["vhost", "user"], + ReqData, Context). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). + +%%-------------------------------------------------------------------- + +permissions() -> + rabbit_auth_backend_internal:list_permissions(). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_permissions_user.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_permissions_user.erl new file mode 100644 index 0000000000..ad1a2e5f96 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_permissions_user.erl @@ -0,0 +1,47 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_permissions_user). + +-export([init/2, to_json/2, content_types_provided/2, resource_exists/2, + is_authorized/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case rabbit_mgmt_wm_user:user(ReqData) of + {ok, _} -> true; + {error, _} -> false + end, ReqData, Context}. + +to_json(ReqData, Context) -> + User = rabbit_mgmt_util:id(user, ReqData), + rabbit_mgmt_util:catch_no_such_user_or_vhost( + fun() -> + Perms = rabbit_auth_backend_internal:list_user_permissions(User), + rabbit_mgmt_util:reply_list([[{user, User} | Rest] || Rest <- Perms], + ["vhost", "user"], ReqData, Context) + end, + fun() -> + rabbit_mgmt_util:bad_request(vhost_or_user_not_found, ReqData, Context) + end). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_permissions_vhost.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_permissions_vhost.erl new file mode 100644 index 0000000000..435c15faa4 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_permissions_vhost.erl @@ -0,0 +1,44 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_permissions_vhost). + +-export([init/2, to_json/2, content_types_provided/2, resource_exists/2, + is_authorized/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {rabbit_vhost:exists(rabbit_mgmt_wm_vhost:id(ReqData)), ReqData, Context}. + +to_json(ReqData, Context) -> + VHost = rabbit_mgmt_util:id(vhost, ReqData), + rabbit_mgmt_util:catch_no_such_user_or_vhost( + fun() -> + Perms = rabbit_auth_backend_internal:list_vhost_permissions(VHost), + rabbit_mgmt_util:reply_list([[{vhost, VHost} | Rest] || Rest <- Perms], + ["vhost", "user"], ReqData, Context) + end, + fun() -> + rabbit_mgmt_util:bad_request(vhost_or_user_not_found, ReqData, Context) + end). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_policies.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_policies.erl new file mode 100644 index 0000000000..c2b1bf1a1a --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_policies.erl @@ -0,0 +1,49 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_policies). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2, + resource_exists/2, basic/1]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case basic(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply_list( + rabbit_mgmt_util:filter_vhost(basic(ReqData), ReqData, Context), + ["priority"], ReqData, Context). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_vhost(ReqData, Context). + +%%-------------------------------------------------------------------- + +basic(ReqData) -> + case rabbit_mgmt_util:vhost(ReqData) of + not_found -> not_found; + none -> rabbit_policy:list(); + VHost -> rabbit_policy:list(VHost) + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_policy.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_policy.erl new file mode 100644 index 0000000000..5ab8033925 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_policy.erl @@ -0,0 +1,82 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_policy). + +-export([init/2, resource_exists/2, to_json/2, + content_types_provided/2, content_types_accepted/2, + is_authorized/2, allowed_methods/2, accept_content/2, + delete_resource/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case policy(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply(policy(ReqData), ReqData, Context). + +accept_content(ReqData0, Context = #context{user = #user{username = Username}}) -> + case rabbit_mgmt_util:vhost(ReqData0) of + not_found -> + rabbit_mgmt_util:not_found(vhost_not_found, ReqData0, Context); + VHost -> + rabbit_mgmt_util:with_decode( + [pattern, definition], ReqData0, Context, + fun([Pattern, Definition], Body, ReqData) -> + case rabbit_policy:set( + VHost, name(ReqData), Pattern, + maps:to_list(Definition), + maps:get(priority, Body, undefined), + maps:get('apply-to', Body, undefined), + Username) of + ok -> + {true, ReqData, Context}; + {error_string, Reason} -> + rabbit_mgmt_util:bad_request( + rabbit_mgmt_format:escape_html_tags(Reason), ReqData, Context) + end + end) + end. + +delete_resource(ReqData, Context = #context{user = #user{username = Username}}) -> + ok = rabbit_policy:delete( + rabbit_mgmt_util:vhost(ReqData), name(ReqData), Username), + {true, ReqData, Context}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_policies(ReqData, Context). + +%%-------------------------------------------------------------------- + +policy(ReqData) -> + rabbit_policy:lookup( + rabbit_mgmt_util:vhost(ReqData), name(ReqData)). + +name(ReqData) -> rabbit_mgmt_util:id(name, ReqData). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_queue.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_queue.erl new file mode 100644 index 0000000000..6560be1524 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_queue.erl @@ -0,0 +1,113 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_queue). + +-export([init/2, resource_exists/2, to_json/2, + content_types_provided/2, content_types_accepted/2, + is_authorized/2, allowed_methods/2, accept_content/2, + delete_resource/2, queue/1, queue/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case queue(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + try + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + [Q] = rabbit_mgmt_db:augment_queues( + [queue(ReqData)], rabbit_mgmt_util:range_ceil(ReqData), + full), + Payload = rabbit_mgmt_format:clean_consumer_details( + rabbit_mgmt_format:strip_pids(Q)), + rabbit_mgmt_util:reply(ensure_defaults(Payload), ReqData, Context); + true -> + rabbit_mgmt_util:reply(rabbit_mgmt_format:strip_pids(queue(ReqData)), + ReqData, Context) + end + catch + {error, invalid_range_parameters, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, Context) + end. + +accept_content(ReqData, Context) -> + Name = rabbit_mgmt_util:id(queue, ReqData), + rabbit_mgmt_util:direct_request( + 'queue.declare', + fun rabbit_mgmt_format:format_accept_content/1, + [{queue, Name}], "Declare queue error: ~s", ReqData, Context). + +delete_resource(ReqData, Context) -> + %% We need to retrieve manually if-unused and if-empty, as the HTTP API uses '-' + %% while the record uses '_' + IfUnused = <<"true">> =:= rabbit_mgmt_util:qs_val(<<"if-unused">>, ReqData), + IfEmpty = <<"true">> =:= rabbit_mgmt_util:qs_val(<<"if-empty">>, ReqData), + Name = rabbit_mgmt_util:id(queue, ReqData), + rabbit_mgmt_util:direct_request( + 'queue.delete', + fun rabbit_mgmt_format:format_accept_content/1, + [{queue, Name}, + {if_unused, IfUnused}, + {if_empty, IfEmpty}], "Delete queue error: ~s", ReqData, Context). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_vhost(ReqData, Context). + +%%-------------------------------------------------------------------- + +%% this is here to ensure certain data points are always there. When a queue +%% is moved there can be transient periods where certain advanced metrics aren't +%% yet available on the new node. +ensure_defaults(Payload0) -> + case lists:keyfind(garbage_collection, 1, Payload0) of + {_K, _V} -> Payload0; + false -> + [{garbage_collection, + [{max_heap_size,-1}, + {min_bin_vheap_size,-1}, + {min_heap_size,-1}, + {fullsweep_after,-1}, + {minor_gcs,-1}]} | Payload0] + end. + +queue(ReqData) -> + case rabbit_mgmt_util:vhost(ReqData) of + not_found -> not_found; + VHost -> queue(VHost, rabbit_mgmt_util:id(queue, ReqData)) + end. + + +queue(VHost, QName) -> + Name = rabbit_misc:r(VHost, queue, QName), + case rabbit_amqqueue:lookup(Name) of + {ok, Q} -> rabbit_mgmt_format:queue(Q); + {error, not_found} -> not_found + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_queue_actions.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_queue_actions.erl new file mode 100644 index 0000000000..c3b13f40c2 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_queue_actions.erl @@ -0,0 +1,68 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2011-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_queue_actions). + +-export([init/2, resource_exists/2, is_authorized/2, + allowed_methods/2, content_types_accepted/2, accept_content/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). +-include_lib("rabbit/include/amqqueue.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"POST">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case rabbit_mgmt_wm_queue:queue(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +accept_content(ReqData, Context) -> + rabbit_mgmt_util:post_respond(do_it(ReqData, Context)). + +do_it(ReqData0, Context) -> + VHost = rabbit_mgmt_util:vhost(ReqData0), + QName = rabbit_mgmt_util:id(queue, ReqData0), + rabbit_mgmt_util:with_decode( + [action], ReqData0, Context, + fun([Action], _Body, ReqData) -> + rabbit_amqqueue:with( + rabbit_misc:r(VHost, queue, QName), + fun(Q) -> action(Action, Q, ReqData, Context) end) + end). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). + +%%-------------------------------------------------------------------- + +action(<<"sync">>, Q, ReqData, Context) when ?is_amqqueue(Q) -> + QPid = amqqueue:get_pid(Q), + spawn(fun() -> rabbit_amqqueue:sync_mirrors(QPid) end), + {true, ReqData, Context}; + +action(<<"cancel_sync">>, Q, ReqData, Context) when ?is_amqqueue(Q) -> + QPid = amqqueue:get_pid(Q), + _ = rabbit_amqqueue:cancel_sync_mirrors(QPid), + {true, ReqData, Context}; + +action(Else, _Q, ReqData, Context) -> + rabbit_mgmt_util:bad_request({unknown, Else}, ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_queue_get.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_queue_get.erl new file mode 100644 index 0000000000..88ad9a0034 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_queue_get.erl @@ -0,0 +1,166 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2011-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_queue_get). + +-export([init/2, resource_exists/2, is_authorized/2, + allowed_methods/2, accept_content/2, content_types_provided/2, + content_types_accepted/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"POST">>, <<"OPTIONS">>], ReqData, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case rabbit_mgmt_wm_queue:queue(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +accept_content(ReqData, Context) -> + rabbit_mgmt_util:post_respond(do_it(ReqData, Context)). + +do_it(ReqData0, Context) -> + VHost = rabbit_mgmt_util:vhost(ReqData0), + Q = rabbit_mgmt_util:id(queue, ReqData0), + rabbit_mgmt_util:with_decode( + [ackmode, count, encoding], ReqData0, Context, + fun([AckModeBin, CountBin, EncBin], Body, ReqData) -> + rabbit_mgmt_util:with_channel( + VHost, ReqData, Context, + fun (Ch) -> + AckMode = list_to_atom(binary_to_list(AckModeBin)), + Count = rabbit_mgmt_util:parse_int(CountBin), + Enc = case EncBin of + <<"auto">> -> auto; + <<"base64">> -> base64; + _ -> throw({error, <<"Unsupported encoding. Please use auto or base64.">>}) + end, + Trunc = case maps:get(truncate, Body, undefined) of + undefined -> none; + TruncBin -> rabbit_mgmt_util:parse_int( + TruncBin) + end, + + Reply = basic_gets(Count, Ch, Q, AckMode, Enc, Trunc), + maybe_rejects(Reply, Ch, AckMode), + rabbit_mgmt_util:reply(remove_delivery_tag(Reply), + ReqData, Context) + end) + end). + + + + +basic_gets(0, _, _, _, _, _) -> + []; + +basic_gets(Count, Ch, Q, AckMode, Enc, Trunc) -> + case basic_get(Ch, Q, AckMode, Enc, Trunc) of + none -> []; + M -> [M | basic_gets(Count - 1, Ch, Q, AckMode, Enc, Trunc)] + end. + + + +ackmode_to_requeue(reject_requeue_false) -> false; +ackmode_to_requeue(reject_requeue_true) -> true. + +parse_ackmode(ack_requeue_false) -> true; +parse_ackmode(ack_requeue_true) -> false; +parse_ackmode(reject_requeue_false) -> false; +parse_ackmode(reject_requeue_true) -> false. + + +% the messages must rejects later, +% because we get always the same message if the +% messages are requeued inside basic_get/5 +maybe_rejects(R, Ch, AckMode) -> + lists:foreach(fun(X) -> + maybe_reject(Ch, AckMode, + proplists:get_value(delivery_tag, X)) + end, R). + +% removes the delivery_tag from the reply. +% it is not necessary +remove_delivery_tag([]) -> []; +remove_delivery_tag([H|T]) -> + [proplists:delete(delivery_tag, H) | [X || X <- remove_delivery_tag(T)]]. + + +maybe_reject(Ch, AckMode, DeliveryTag) when AckMode == reject_requeue_true; + AckMode == reject_requeue_false -> + amqp_channel:call(Ch, + #'basic.reject'{delivery_tag = DeliveryTag, + requeue = ackmode_to_requeue(AckMode)}); +maybe_reject(_Ch, _AckMode, _DeliveryTag) -> ok. + + +basic_get(Ch, Q, AckMode, Enc, Trunc) -> + case amqp_channel:call(Ch, + #'basic.get'{queue = Q, + no_ack = parse_ackmode(AckMode)}) of + {#'basic.get_ok'{redelivered = Redelivered, + exchange = Exchange, + routing_key = RoutingKey, + message_count = MessageCount, + delivery_tag = DeliveryTag}, + #amqp_msg{props = Props, payload = Payload}} -> + [{payload_bytes, size(Payload)}, + {redelivered, Redelivered}, + {exchange, Exchange}, + {routing_key, RoutingKey}, + {message_count, MessageCount}, + {delivery_tag, DeliveryTag}, + {properties, rabbit_mgmt_format:basic_properties(Props)}] ++ + payload_part(maybe_truncate(Payload, Trunc), Enc); + #'basic.get_empty'{} -> + none + end. + + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_vhost(ReqData, Context). + +%%-------------------------------------------------------------------- + +maybe_truncate(Payload, none) -> Payload; +maybe_truncate(Payload, Len) when size(Payload) < Len -> Payload; +maybe_truncate(Payload, Len) -> + <<Start:Len/binary, _Rest/binary>> = Payload, + Start. + +payload_part(Payload, Enc) -> + {PL, E} = case Enc of + auto -> case is_utf8(Payload) of + true -> {Payload, string}; + false -> {base64:encode(Payload), base64} + end; + _ -> {base64:encode(Payload), base64} + end, + [{payload, PL}, {payload_encoding, E}]. + +is_utf8(<<>>) -> true; +is_utf8(<<_/utf8, Rest/bits>>) -> is_utf8(Rest); +is_utf8(_) -> false. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_queue_purge.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_queue_purge.erl new file mode 100644 index 0000000000..b250984d11 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_queue_purge.erl @@ -0,0 +1,42 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_queue_purge). + +-export([init/2, resource_exists/2, is_authorized/2, allowed_methods/2, + delete_resource/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case rabbit_mgmt_wm_queue:queue(ReqData) of + not_found -> false; + _ -> true + end, ReqData, Context}. + +delete_resource(ReqData, Context) -> + Name = rabbit_mgmt_util:id(queue, ReqData), + rabbit_mgmt_util:direct_request( + 'queue.purge', + fun rabbit_mgmt_format:format_accept_content/1, + [{queue, Name}], "Error purging queue: ~s", ReqData, Context). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_vhost(ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_queues.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_queues.erl new file mode 100644 index 0000000000..7df50f61d9 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_queues.erl @@ -0,0 +1,113 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_queues). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2, + resource_exists/2, basic/1]). +-export([variances/2, + augmented/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include_lib("rabbit/include/amqqueue.hrl"). + +-define(BASIC_COLUMNS, ["vhost", "name", "durable", "auto_delete", "exclusive", + "owner_pid", "arguments", "pid", "state"]). + +-define(DEFAULT_SORT, ["vhost", "name"]). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case queues0(ReqData) of + vhost_not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + try + Basic = basic_vhost_filtered(ReqData, Context), + Data = rabbit_mgmt_util:augment_resources(Basic, ?DEFAULT_SORT, + ?BASIC_COLUMNS, ReqData, + Context, fun augment/2), + rabbit_mgmt_util:reply(Data, ReqData, Context) + catch + {error, invalid_range_parameters, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, + Context) + end. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_vhost(ReqData, Context). + +%%-------------------------------------------------------------------- +%% Exported functions + +basic(ReqData) -> + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + [rabbit_mgmt_format:queue(Q) || Q <- queues0(ReqData)] ++ + [rabbit_mgmt_format:queue(amqqueue:set_state(Q, down)) || + Q <- down_queues(ReqData)]; + true -> + case rabbit_mgmt_util:enable_queue_totals(ReqData) of + false -> + [rabbit_mgmt_format:queue(Q) ++ policy(Q) || Q <- queues0(ReqData)] ++ + [rabbit_mgmt_format:queue(amqqueue:set_state(Q, down)) || + Q <- down_queues(ReqData)]; + true -> + [rabbit_mgmt_format:queue_info(Q) || Q <- queues_with_totals(ReqData)] ++ + [rabbit_mgmt_format:queue(amqqueue:set_state(Q, down)) || + Q <- down_queues(ReqData)] + end + end. + +augmented(ReqData, Context) -> + augment(rabbit_mgmt_util:filter_vhost(basic(ReqData), ReqData, Context), ReqData). + +%%-------------------------------------------------------------------- +%% Private helpers + +augment(Basic, ReqData) -> + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + rabbit_mgmt_db:augment_queues(Basic, rabbit_mgmt_util:range_ceil(ReqData), + basic); + true -> + Basic + end. + +basic_vhost_filtered(ReqData, Context) -> + rabbit_mgmt_util:filter_vhost(basic(ReqData), ReqData, Context). + +queues0(ReqData) -> + rabbit_mgmt_util:all_or_one_vhost(ReqData, fun rabbit_amqqueue:list/1). + +queues_with_totals(ReqData) -> + rabbit_mgmt_util:all_or_one_vhost(ReqData, fun collect_info_all/1). + +collect_info_all(VHostPath) -> + rabbit_amqqueue:collect_info_all(VHostPath, [name, durable, auto_delete, exclusive, owner_pid, arguments, type, state, policy, totals, type_specific]). + +down_queues(ReqData) -> + rabbit_mgmt_util:all_or_one_vhost(ReqData, fun rabbit_amqqueue:list_down/1). + +policy(Q) -> + case rabbit_policy:name(Q) of + none -> []; + Policy -> [{policy, Policy}] + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_rebalance_queues.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_rebalance_queues.erl new file mode 100644 index 0000000000..982655493c --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_rebalance_queues.erl @@ -0,0 +1,58 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_rebalance_queues). + +-export([init/2, service_available/2, resource_exists/2, + content_types_provided/2, content_types_accepted/2, + is_authorized/2, allowed_methods/2, accept_content/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, [Mode]) -> + Headers = rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), + {cowboy_rest, Headers, {Mode, #context{}}}. + +service_available(Req, {{queues, all}, _Context}=State) -> + {true, Req, State}; +service_available(Req, State) -> + {false, Req, State}. + +variances(Req, State) -> + {[<<"accept-encoding">>, <<"origin">>], Req, State}. + +content_types_provided(Req, State) -> + {[{{<<"text">>, <<"plain">>, '*'}, undefined}], Req, State}. + +content_types_accepted(Req, State) -> + {[{'*', accept_content}], Req, State}. + +allowed_methods(Req, State) -> + {[<<"POST">>, <<"OPTIONS">>], Req, State}. + +resource_exists(Req, State) -> + {true, Req, State}. + +accept_content(Req, {_Mode, #context{user = #user{username = Username}}}=State) -> + try + rabbit_log:info("User '~s' has initiated a queue rebalance", [Username]), + spawn(fun() -> + rabbit_amqqueue:rebalance(all, <<".*">>, <<".*">>) + end), + {true, Req, State} + catch + {error, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), Req, State) + end. + +is_authorized(Req0, {Mode, Context0}) -> + {Res, Req1, Context1} = rabbit_mgmt_util:is_authorized_admin(Req0, Context0), + {Res, Req1, {Mode, Context1}}. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_redirect.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_redirect.erl new file mode 100644 index 0000000000..c5b3808d5a --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_redirect.erl @@ -0,0 +1,13 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2010-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_redirect). +-export([init/2]). + +init(Req0, RedirectTo) -> + Req = cowboy_req:reply(301, #{<<"location">> => RedirectTo}, Req0), + {ok, Req, RedirectTo}. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_reset.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_reset.erl new file mode 100644 index 0000000000..ea3d698ece --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_reset.erl @@ -0,0 +1,54 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_reset). + +-export([init/2, is_authorized/2, resource_exists/2, + allowed_methods/2, delete_resource/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + case get_node(ReqData) of + none -> {true, ReqData, Context}; + {ok, Node} -> {lists:member(Node, rabbit_nodes:all_running()), + ReqData, Context} + end. + +delete_resource(ReqData, Context) -> + case get_node(ReqData) of + none -> + rabbit_mgmt_storage:reset_all(); + {ok, Node} -> + rpc:call(Node, rabbit_mgmt_storage, reset, []) + end, + {true, ReqData, Context}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). + + +get_node(ReqData) -> + case rabbit_mgmt_util:id(node, ReqData) of + none -> + none; + Node0 -> + Node = list_to_atom(binary_to_list(Node0)), + {ok, Node} + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_static.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_static.erl new file mode 100644 index 0000000000..4a7f608a8a --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_static.erl @@ -0,0 +1,65 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2010-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +%% Alias for cowboy_static that accepts a list of directories +%% where static files can be found. + +-module(rabbit_mgmt_wm_static). + +-include_lib("kernel/include/file.hrl"). + +-export([init/2]). +-export([malformed_request/2]). +-export([forbidden/2]). +-export([content_types_provided/2]). +-export([resource_exists/2]). +-export([last_modified/2]). +-export([generate_etag/2]). +-export([get_file/2]). + + +init(Req0, {priv_file, _App, _Path}=Opts) -> + Req1 = rabbit_mgmt_headers:set_common_permission_headers(Req0, ?MODULE), + cowboy_static:init(Req1, Opts); +init(Req0, [{App, Path}]) -> + Req1 = rabbit_mgmt_headers:set_common_permission_headers(Req0, ?MODULE), + do_init(Req1, App, Path); +init(Req0, [{App, Path}|Tail]) -> + Req1 = rabbit_mgmt_headers:set_common_permission_headers(Req0, ?MODULE), + PathInfo = cowboy_req:path_info(Req1), + Filepath = filename:join([code:priv_dir(App), Path|PathInfo]), + %% We use erl_prim_loader because the file may be inside an .ez archive. + FileInfo = erl_prim_loader:read_file_info(binary_to_list(Filepath)), + case FileInfo of + {ok, #file_info{type = regular}} -> do_init(Req1, App, Path); + {ok, #file_info{type = symlink}} -> do_init(Req1, App, Path); + _ -> init(Req0, Tail) + end. + +do_init(Req, App, Path) -> + cowboy_static:init(Req, {priv_dir, App, Path}). + +malformed_request(Req, State) -> + cowboy_static:malformed_request(Req, State). + +forbidden(Req, State) -> + cowboy_static:forbidden(Req, State). + +content_types_provided(Req, State) -> + cowboy_static:content_types_provided(Req, State). + +resource_exists(Req, State) -> + cowboy_static:resource_exists(Req, State). + +last_modified(Req, State) -> + cowboy_static:last_modified(Req, State). + +generate_etag(Req, State) -> + cowboy_static:generate_etag(Req, State). + +get_file(Req, State) -> + cowboy_static:get_file(Req, State). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_topic_permission.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_topic_permission.erl new file mode 100644 index 0000000000..99f25bb69e --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_topic_permission.erl @@ -0,0 +1,103 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_topic_permission). + +-export([init/2, resource_exists/2, to_json/2, + content_types_provided/2, content_types_accepted/2, + is_authorized/2, allowed_methods/2, accept_content/2, + delete_resource/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {[{<<"application/json">>, to_json}], ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case topic_perms(ReqData) of + none -> false; + not_found -> false; + _ -> true + end, ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply(topic_perms(ReqData), ReqData, Context). + +accept_content(ReqData0, Context = #context{user = #user{username = Username}}) -> + case topic_perms(ReqData0) of + not_found -> + rabbit_mgmt_util:bad_request(vhost_or_user_not_found, + ReqData0, Context); + _ -> + User = rabbit_mgmt_util:id(user, ReqData0), + VHost = rabbit_mgmt_util:id(vhost, ReqData0), + rabbit_mgmt_util:with_decode( + [exchange, write, read], ReqData0, Context, + fun([Exchange, Write, Read], _, ReqData) -> + rabbit_auth_backend_internal:set_topic_permissions( + User, VHost, Exchange, Write, Read, Username), + {true, ReqData, Context} + end) + end. + +delete_resource(ReqData, Context = #context{user = #user{username = Username}}) -> + User = rabbit_mgmt_util:id(user, ReqData), + VHost = rabbit_mgmt_util:id(vhost, ReqData), + case rabbit_mgmt_util:id(exchange, ReqData) of + none -> + rabbit_auth_backend_internal:clear_topic_permissions(User, VHost, Username); + Exchange -> + rabbit_auth_backend_internal:clear_topic_permissions(User, VHost, Exchange, + Username) + end, + {true, ReqData, Context}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). + +%%-------------------------------------------------------------------- + +topic_perms(ReqData) -> + User = rabbit_mgmt_util:id(user, ReqData), + case rabbit_auth_backend_internal:lookup_user(User) of + {ok, _} -> + case rabbit_mgmt_util:vhost(ReqData) of + not_found -> + not_found; + VHost -> + rabbit_mgmt_util:catch_no_such_user_or_vhost( + fun() -> + Perms = + rabbit_auth_backend_internal:list_user_vhost_topic_permissions( + User, VHost), + case Perms of + [] -> none; + TopicPermissions -> [[{user, User}, {vhost, VHost} | TopicPermission] + || TopicPermission <- TopicPermissions] + end + end, + fun() -> not_found end) + end; + {error, _} -> + not_found + end. diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_topic_permissions.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_topic_permissions.erl new file mode 100644 index 0000000000..c1406043cd --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_topic_permissions.erl @@ -0,0 +1,38 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_topic_permissions). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([topic_permissions/0]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {[{<<"application/json">>, to_json}], ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply_list(topic_permissions(), ["vhost", "user"], + ReqData, Context). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). + +%%-------------------------------------------------------------------- + +topic_permissions() -> + rabbit_auth_backend_internal:list_topic_permissions(). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_topic_permissions_user.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_topic_permissions_user.erl new file mode 100644 index 0000000000..7d9c2d58fc --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_topic_permissions_user.erl @@ -0,0 +1,47 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_topic_permissions_user). + +-export([init/2, to_json/2, content_types_provided/2, resource_exists/2, + is_authorized/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {[{<<"application/json">>, to_json}], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case rabbit_mgmt_wm_user:user(ReqData) of + {ok, _} -> true; + {error, _} -> false + end, ReqData, Context}. + +to_json(ReqData, Context) -> + User = rabbit_mgmt_util:id(user, ReqData), + rabbit_mgmt_util:catch_no_such_user_or_vhost( + fun() -> + Perms = rabbit_auth_backend_internal:list_user_topic_permissions(User), + rabbit_mgmt_util:reply_list([[{user, User} | Rest] || Rest <- Perms], + ["vhost", "user"], ReqData, Context) + end, + fun() -> + rabbit_mgmt_util:bad_request(vhost_or_user_not_found, ReqData, Context) + end). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_topic_permissions_vhost.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_topic_permissions_vhost.erl new file mode 100644 index 0000000000..8540ee9a77 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_topic_permissions_vhost.erl @@ -0,0 +1,44 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_topic_permissions_vhost). + +-export([init/2, to_json/2, content_types_provided/2, resource_exists/2, + is_authorized/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {[{<<"application/json">>, to_json}], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {rabbit_vhost:exists(rabbit_mgmt_wm_vhost:id(ReqData)), ReqData, Context}. + +to_json(ReqData, Context) -> + VHost = rabbit_mgmt_util:id(vhost, ReqData), + rabbit_mgmt_util:catch_no_such_user_or_vhost( + fun() -> + Perms = rabbit_auth_backend_internal:list_vhost_topic_permissions(VHost), + rabbit_mgmt_util:reply_list([[{vhost, VHost} | Rest] || Rest <- Perms], + ["vhost", "user"], ReqData, Context) + end, + fun() -> + rabbit_mgmt_util:bad_request(vhost_or_user_not_found, ReqData, Context) + end). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_user.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_user.erl new file mode 100644 index 0000000000..4c014565f1 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_user.erl @@ -0,0 +1,73 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_user). + +-export([init/2, resource_exists/2, to_json/2, + content_types_provided/2, content_types_accepted/2, + is_authorized/2, allowed_methods/2, accept_content/2, + delete_resource/2, user/1, put_user/2, put_user/3]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case user(ReqData) of + {ok, _} -> true; + {error, _} -> false + end, ReqData, Context}. + +to_json(ReqData, Context) -> + {ok, User} = user(ReqData), + rabbit_mgmt_util:reply(rabbit_mgmt_format:internal_user(User), + ReqData, Context). + +accept_content(ReqData0, Context = #context{user = #user{username = ActingUser}}) -> + Username = rabbit_mgmt_util:id(user, ReqData0), + rabbit_mgmt_util:with_decode( + [], ReqData0, Context, + fun(_, User, ReqData) -> + put_user(User#{name => Username}, ActingUser), + {true, ReqData, Context} + end). + +delete_resource(ReqData, Context = #context{user = #user{username = ActingUser}}) -> + User = rabbit_mgmt_util:id(user, ReqData), + rabbit_auth_backend_internal:delete_user(User, ActingUser), + {true, ReqData, Context}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). + +%%-------------------------------------------------------------------- + +user(ReqData) -> + rabbit_auth_backend_internal:lookup_user(rabbit_mgmt_util:id(user, ReqData)). + +put_user(User, ActingUser) -> + put_user(User, undefined, ActingUser). + +put_user(User, Version, ActingUser) -> + rabbit_auth_backend_internal:put_user(User, Version, ActingUser). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_user_limit.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_user_limit.erl new file mode 100644 index 0000000000..8e59c55371 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_user_limit.erl @@ -0,0 +1,62 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_user_limit). + +-export([init/2, + content_types_accepted/2, is_authorized/2, + allowed_methods/2, accept_content/2, + delete_resource/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_accepted(ReqData, Context) -> + {[{{<<"application">>, <<"json">>, '*'}, accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +accept_content(ReqData0, Context = #context{user = #user{username = ActingUser}}) -> + case rabbit_mgmt_util:id(user, ReqData0) of + not_found -> + rabbit_mgmt_util:not_found(user_not_found, ReqData0, Context); + Username -> + rabbit_mgmt_util:with_decode( + [value], ReqData0, Context, + fun([Value], _Body, ReqData) -> + Limit = #{name(ReqData) => Value}, + case rabbit_auth_backend_internal:set_user_limits(Username, Limit, ActingUser) of + ok -> + {true, ReqData, Context}; + {error_string, Reason} -> + rabbit_mgmt_util:bad_request( + list_to_binary(Reason), ReqData, Context) + end + end) + end. + +delete_resource(ReqData, Context = #context{user = #user{username = ActingUser}}) -> + ok = rabbit_auth_backend_internal:clear_user_limits( + rabbit_mgmt_util:id(user, ReqData), name(ReqData), ActingUser), + {true, ReqData, Context}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). + +%%-------------------------------------------------------------------- + +name(ReqData) -> rabbit_mgmt_util:id(name, ReqData). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_user_limits.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_user_limits.erl new file mode 100644 index 0000000000..7e4d541f32 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_user_limits.erl @@ -0,0 +1,63 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_user_limits). + +-export([init/2, to_json/2, content_types_provided/2, + resource_exists/2, is_authorized/2, allowed_methods/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"GET">>, <<"OPTIONS">>], ReqData, Context}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_vhost_visible(ReqData, Context). + +content_types_provided(ReqData, Context) -> + {[{<<"application/json">>, to_json}], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {case user(ReqData) of + none -> true; + Username -> rabbit_auth_backend_internal:exists(Username) + end, ReqData, Context}. + +to_json(ReqData, Context) -> + rabbit_mgmt_util:reply_list(limits(ReqData, Context), [], ReqData, Context). + +limits(ReqData, _Context) -> + case user(ReqData) of + none -> + [ [{user, internal_user:get_username(U)}, {value, maps:to_list(internal_user:get_limits(U))}] + || U <- rabbit_auth_backend_internal:all_users(), + internal_user:get_limits(U) =/= #{}]; + Username when is_binary(Username) -> + case rabbit_auth_backend_internal:get_user_limits(Username) of + Value when is_map(Value) -> + case maps:size(Value) of + 0 -> []; + _ -> [[{user, Username}, {value, maps:to_list(Value)}]] + end; + undefined -> + [] + end + end. +%%-------------------------------------------------------------------- + +user(ReqData) -> + rabbit_mgmt_util:id(user, ReqData). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_users.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_users.erl new file mode 100644 index 0000000000..67b8345ab9 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_users.erl @@ -0,0 +1,59 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_users). + +-export([init/2, to_json/2, + content_types_provided/2, + is_authorized/2, allowed_methods/2]). +-export([variances/2]). +-export([users/1]). + +-import(rabbit_misc, [pget/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, [Mode]) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), {Mode, #context{}}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"OPTIONS">>], ReqData, Context}. + +to_json(ReqData, {Mode, Context}) -> + rabbit_mgmt_util:reply_list(users(Mode), ReqData, Context). + +is_authorized(ReqData, {Mode, Context}) -> + {Res, Req2, Context2} = rabbit_mgmt_util:is_authorized_admin(ReqData, Context), + {Res, Req2, {Mode, Context2}}. + +%%-------------------------------------------------------------------- + +users(all) -> + [begin + {ok, User} = rabbit_auth_backend_internal:lookup_user(pget(user, U)), + rabbit_mgmt_format:internal_user(User) + end || U <- rabbit_auth_backend_internal:list_users()]; +users(without_permissions) -> + lists:foldl(fun(U, Acc) -> + Username = pget(user, U), + case rabbit_auth_backend_internal:list_user_permissions(Username) of + [] -> + {ok, User} = rabbit_auth_backend_internal:lookup_user(Username), + [rabbit_mgmt_format:internal_user(User) | Acc]; + _ -> + Acc + end + end, [], rabbit_auth_backend_internal:list_users()). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_users_bulk_delete.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_users_bulk_delete.erl new file mode 100644 index 0000000000..6f6a7863a2 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_users_bulk_delete.erl @@ -0,0 +1,47 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_users_bulk_delete). + +-export([init/2, + content_types_accepted/2, + content_types_provided/2, + is_authorized/2, + allowed_methods/2, accept_content/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"POST">>, <<"OPTIONS">>], ReqData, Context}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). + +accept_content(ReqData0, Context = #context{user = #user{username = ActingUser}}) -> + rabbit_mgmt_util:with_decode( + [users], ReqData0, Context, + fun([Users], _, ReqData) -> + [rabbit_auth_backend_internal:delete_user(User, ActingUser) + || User <- Users], + {true, ReqData, Context} + end). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_vhost.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_vhost.erl new file mode 100644 index 0000000000..e62eecac97 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_vhost.erl @@ -0,0 +1,97 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_vhost). + +-export([init/2, resource_exists/2, to_json/2, + content_types_provided/2, content_types_accepted/2, + is_authorized/2, allowed_methods/2, accept_content/2, + delete_resource/2, id/1, put_vhost/5]). +-export([variances/2]). + +-import(rabbit_misc, [pget/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"HEAD">>, <<"GET">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {rabbit_vhost:exists(id(ReqData)), ReqData, Context}. + +to_json(ReqData, Context) -> + try + Id = id(ReqData), + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + rabbit_mgmt_util:reply( + hd(rabbit_mgmt_db:augment_vhosts( + [rabbit_vhost:info(Id)], rabbit_mgmt_util:range(ReqData))), + ReqData, Context); + true -> + rabbit_mgmt_util:reply(rabbit_vhost:info(Id), ReqData, Context) + end + catch + {error, invalid_range_parameters, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, Context) + end. + +accept_content(ReqData0, Context = #context{user = #user{username = Username}}) -> + Name = id(ReqData0), + rabbit_mgmt_util:with_decode( + [], ReqData0, Context, + fun(_, BodyMap, ReqData) -> + Trace = rabbit_mgmt_util:parse_bool(maps:get(tracing, BodyMap, undefined)), + Description = maps:get(description, BodyMap, <<"">>), + Tags = maps:get(tags, BodyMap, <<"">>), + case put_vhost(Name, Description, Tags, Trace, Username) of + ok -> + {true, ReqData, Context}; + {error, timeout} = E -> + rabbit_mgmt_util:internal_server_error( + "Timed out while waiting for the vhost to initialise", E, + ReqData0, Context) + end + end). + +delete_resource(ReqData, Context = #context{user = #user{username = Username}}) -> + VHost = id(ReqData), + try + rabbit_vhost:delete(VHost, Username) + catch _:{error, {no_such_vhost, _}} -> + ok + end, + {true, ReqData, Context}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). + +%%-------------------------------------------------------------------- + +id(ReqData) -> + case rabbit_mgmt_util:id(vhost, ReqData) of + [Value] -> Value; + Value -> Value + end. + +put_vhost(Name, Description, Tags, Trace, Username) -> + rabbit_vhost:put_vhost(Name, Description, Tags, Trace, Username). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_vhost_restart.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_vhost_restart.erl new file mode 100644 index 0000000000..e22ff058c7 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_vhost_restart.erl @@ -0,0 +1,56 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2011-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_vhost_restart). + +-export([init/2, resource_exists/2, is_authorized/2, + allowed_methods/2, content_types_accepted/2, accept_content/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"POST">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + VHost = id(ReqData), + {rabbit_vhost:exists(VHost), ReqData, Context}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +accept_content(ReqData, Context) -> + VHost = id(ReqData), + NodeB = rabbit_mgmt_util:id(node, ReqData), + Node = binary_to_atom(NodeB, utf8), + case rabbit_vhost_sup_sup:start_vhost(VHost, Node) of + {ok, _} -> + {true, ReqData, Context}; + {error, {already_started, _}} -> + {true, ReqData, Context}; + {error, Err} -> + Message = io_lib:format("Request to node ~s failed with ~p", + [Node, Err]), + rabbit_mgmt_util:bad_request(list_to_binary(Message), ReqData, Context) + end. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized_admin(ReqData, Context). + +%%-------------------------------------------------------------------- + +id(ReqData) -> + rabbit_mgmt_util:id(vhost, ReqData). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_vhosts.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_vhosts.erl new file mode 100644 index 0000000000..7380faa4f0 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_vhosts.erl @@ -0,0 +1,69 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_vhosts). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([variances/2]). +-export([basic/0, augmented/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +-define(BASIC_COLUMNS, ["name", "tracing", "pid"]). + +-define(DEFAULT_SORT, ["name"]). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +to_json(ReqData, Context = #context{user = User}) -> + try + Basic = [rabbit_vhost:info(V) + || V <- rabbit_mgmt_util:list_visible_vhosts(User)], + Data = rabbit_mgmt_util:augment_resources(Basic, ?DEFAULT_SORT, + ?BASIC_COLUMNS, ReqData, + Context, fun augment/2), + rabbit_mgmt_util:reply(Data, ReqData, Context) + catch + {error, invalid_range_parameters, Reason} -> + rabbit_mgmt_util:bad_request(iolist_to_binary(Reason), ReqData, Context) + end. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). + +%%-------------------------------------------------------------------- + +augment(Basic, ReqData) -> + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + rabbit_mgmt_db:augment_vhosts(Basic, rabbit_mgmt_util:range(ReqData)); + true -> + Basic + end. + +augmented(ReqData, #context{user = User}) -> + case rabbit_mgmt_util:disable_stats(ReqData) of + false -> + rabbit_mgmt_db:augment_vhosts( + [rabbit_vhost:info(V) || V <- rabbit_mgmt_util:list_visible_vhosts(User)], + rabbit_mgmt_util:range(ReqData)); + true -> + [rabbit_vhost:info(V) || V <- rabbit_mgmt_util:list_visible_vhosts(User)] + end. + +basic() -> + rabbit_vhost:info_all([name]). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_whoami.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_whoami.erl new file mode 100644 index 0000000000..22a1063443 --- /dev/null +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_whoami.erl @@ -0,0 +1,36 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_wm_whoami). + +-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]). +-export([variances/2]). + +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +%%-------------------------------------------------------------------- + +init(Req, _State) -> + {cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}. + +variances(Req, Context) -> + {[<<"accept-encoding">>, <<"origin">>], Req, Context}. + +content_types_provided(ReqData, Context) -> + {rabbit_mgmt_util:responder_map(to_json), ReqData, Context}. + +to_json(ReqData, Context = #context{user = User}) -> + FormattedUser = rabbit_mgmt_format:user(User), + Expiration = case application:get_env(rabbitmq_management, login_session_timeout) of + undefined -> []; + {ok, Val} -> [{login_session_timeout, Val}] + end, + rabbit_mgmt_util:reply(FormattedUser ++ Expiration, ReqData, Context). + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). |