38 |
39 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/forms/bulk_user_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% from 'forms/macros.html' import render_field, render_checkbox_field %}
3 | {% block title %}Add New Machine with ApiKey{% endblock %}
4 | {% block content %}
5 |
58 |
59 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/forms/ipv4_rule.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% from 'forms/macros.html' import render_field %}
3 | {% block title %}Add IPv4 rule{% endblock %}
4 | {% block content %}
5 |
95 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/forms/ipv6_rule.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% from 'forms/macros.html' import render_field %}
3 | {% block title %}Add IPv6 rule{% endblock %}
4 | {% block content %}
5 |
88 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/forms/machine_api_key.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% from 'forms/macros.html' import render_field, render_checkbox_field %}
3 | {% block title %}Add New Machine with ApiKey{% endblock %}
4 | {% block content %}
5 |
7 | In general, the keys should be Read Only and with expiration.
8 | If you need to create a full access Read/Write key, consider using usual user form
9 | with your organization settings.
10 |
34 |
35 | {{ render_field(form.comment) }}
36 |
37 |
38 |
43 |
44 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/forms/macros.html:
--------------------------------------------------------------------------------
1 | {# Renders field for bootstrap 5 standards.
2 |
3 | Params:
4 | field - WTForm field
5 | kwargs - pass any arguments you want in order to put them into the html attributes.
6 | There are few exceptions: for - for_, class - class_, class__ - class_
7 |
8 | Example usage:
9 | {{ macros.render_field(form.email, placeholder='Input email', type='email') }}
10 | #}
11 | {% macro render_field(field, label_visible=true, tooltip=False) -%}
12 |
40 | {%- endmacro %}
41 |
42 |
43 |
44 |
45 |
46 |
47 | {# Renders checkbox fields since they are represented differently in bootstrap
48 | Params:
49 | field - WTForm field (there are no check, but you should put here only BooleanField.
50 | kwargs - pass any arguments you want in order to put them into the html attributes.
51 | There are few exceptions: for - for_, class - class_, class__ - class_
52 |
53 | Example usage:
54 | {{ macros.render_checkbox_field(form.remember_me) }}
55 | #}
56 | {% macro render_checkbox_field(field) -%}
57 |
58 | {{ field(type='checkbox', class_='form-check-input', **kwargs) }}
59 |
60 | {{ field.label }}
61 |
62 |
63 | {%- endmacro %}
64 |
65 | {# Renders radio field
66 | Params:
67 | field - WTForm field (there are no check, but you should put here only BooleanField.
68 | kwargs - pass any arguments you want in order to put them into the html attributes.
69 | There are few exceptions: for - for_, class - class_, class__ - class_
70 |
71 | Example usage:
72 | {{ macros.render_radio_field(form.answers) }}
73 | #}
74 | {% macro render_radio_field(field) -%}
75 | {% for value, label, _ in field.iter_choices() %}
76 |
77 |
78 |
79 | {{ label }}
80 |
81 |
82 | {% endfor %}
83 | {%- endmacro %}
84 |
85 | {# Renders WTForm in bootstrap way. There are two ways to call function:
86 | - as macros: it will render all field forms using cycle to iterate over them
87 | - as call: it will insert form fields as you specify:
88 | e.g. {% call macros.render_form(form, action_url=url_for('login_view'), action_text='Login',
89 | class_='login-form') %}
90 | {{ macros.render_field(form.email, placeholder='Input email', type='email') }}
91 | {{ macros.render_field(form.password, placeholder='Input password', type='password') }}
92 | {{ macros.render_checkbox_field(form.remember_me, type='checkbox') }}
93 | {% endcall %}
94 |
95 | Params:
96 | form - WTForm class
97 | action_url - url where to submit this form
98 | action_text - text of submit button
99 | class_ - sets a class for form
100 | #}
101 | {% macro render_form(form, action_url='', action_text='Submit', class_='', btn_class='btn btn-primary') -%}
102 |
103 |
120 | {%- endmacro %}
--------------------------------------------------------------------------------
/flowapp/templates/forms/org.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% from 'forms/macros.html' import render_field %}
3 | {% block title %}Add / Edit Organization{% endblock %}
4 | {% block content %}
5 |
{{ title or 'New'}} Organization
6 |
35 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/forms/rtbh_rule.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% from 'forms/macros.html' import render_field %}
3 | {% block title %}Add RTBH rule{% endblock %}
4 | {% block content %}
5 |
{{ title or 'New'}} RTBH rule
6 |
69 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/forms/rule.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% block title %}Add IPv4 rule{% endblock %}
3 | {% block content %}
4 |
104 |
105 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/forms/simple_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% from 'forms/macros.html' import render_form %}
3 |
4 | {% block title %}
5 | {{ title }}
6 | {% endblock %}
7 | {% block content %}
8 |
9 | {{ render_form(form, action_url=action_url, action_text='Save') }}
10 |
11 | {% endblock content %}
--------------------------------------------------------------------------------
/flowapp/templates/layouts/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
{% block title %}{% endblock %}
15 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {% block head %}{% endblock %}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
{{ config['APP_NAME'] }}
38 |
39 |
40 |
41 |
42 |
43 | {% if session['can_edit'] %}
44 | {% for item in main_menu['edit'] %}
45 | {{ item.name}}
46 | {% endfor %}
47 | {% endif %}
48 | {% if 3 in session['user_role_ids'] %}
49 |
50 |
51 | Admin
52 |
53 |
63 |
64 | {% endif %}
65 |
66 |
67 | {{ session['user_name']}} <{{ session['user_email'] }}>,
68 | role: {{ session['user_roles']|join(", ") }}, org: {{ session['user_org'] }}
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | {% with messages = get_flashed_messages(with_categories=true) %}
77 | {% if messages %}
78 | {% for category, message in messages %}
79 |
80 | {{ message }}
81 |
83 |
84 | {% endfor %}
85 | {% endif %}
86 | {% endwith %}
87 |
88 | {% block content %}{% endblock %}
89 |
90 |
100 |
ExaFS {{ session['app_version'] }}
101 |
102 |
103 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/flowapp/templates/macros.html:
--------------------------------------------------------------------------------
1 |
2 | {% macro build_ip_tbody(rules, today, editable=True, group_op=True) %}
3 |
4 | {% for rule in rules %}
5 | {% if rule.next_header is defined %}
6 | {% set rtype_int = 6 %}
7 | {% else %}
8 | {% set rtype_int = 4 %}
9 | {% endif %}
10 |
11 |
12 |
13 | {{ rule.source }} {% if rule.source_mask != none %}{{ '/' if rule.source_mask >= 0 else '' }}{{ rule.source_mask if rule.source_mask >= 0 else '' }}{% endif %}
14 |
15 |
16 | {{ rule.source_port }}
17 |
18 |
19 | {{ rule.dest }} {% if rule.dest_mask != none %}{{ '/' if rule.dest_mask >= 0 else '' }}{{ rule.dest_mask if rule.dest_mask >= 0 else '' }}{% endif %}
20 |
21 |
22 | {{ rule.dest_port }}
23 |
24 |
25 | {% if rtype_int == 4 %}
26 | {{ rule.protocol }}
27 | {% elif rtype_int == 6 %}
28 | {{ rule.next_header }}
29 | {% endif %}
30 |
31 |
32 | {{ rule.packet_len }}
33 |
34 |
35 | {{ rule.expires|strftime }}
36 |
37 |
38 | {{ rule.action.name }}
39 |
40 |
41 | {{ rule.flags}}
42 |
43 |
44 | {{ rule.user.name }}
45 |
46 |
47 | {% if editable %}
48 |
49 |
50 |
51 |
52 |
53 |
54 | {% endif %}
55 | {% if rule.comment %}
56 |
57 |
58 |
59 | {% endif %}
60 |
61 |
62 | {% if editable and group_op %}
63 |
64 |
65 |
66 | {% endif %}
67 |
68 |
69 |
70 |
71 | {% endfor %}
72 |
73 | {% endmacro %}
74 |
75 |
76 | {% macro build_rtbh_tbody(rules, today, editable=True, group_op=True) %}
77 |
78 | {% for rule in rules %}
79 |
80 |
81 | {% if rule.ipv4 %}
82 | {{ rule.ipv4 }} {{ '/' if rule.ipv4_mask else '' }}{{rule.ipv4_mask|default("", True)}}
83 | {% endif %}
84 | {% if rule.ipv6 %}
85 | {{ rule.ipv6 }} {{ '/' if rule.ipv6_mask else '' }} {{rule.ipv6_mask|default("", True)}}
86 | {% endif %}
87 |
88 |
89 | {{ rule.community.name }}
90 |
91 |
92 |
93 | {{ rule.expires|strftime }}
94 |
95 |
96 | {{ rule.user.name }}
97 |
98 |
99 | {% if editable %}
100 |
101 |
102 |
103 |
104 |
105 |
106 | {% endif %}
107 | {% if rule.comment %}
108 |
109 |
110 |
111 | {% endif %}
112 |
113 | {% if editable and group_op %}
114 |
115 |
116 |
117 | {% endif %}
118 |
119 |
120 | {% endfor %}
121 |
122 | {% endmacro %}
123 |
124 |
125 | {% macro build_rules_thead(rules_columns, rtype, rstate, sort_key, sort_order, search_query='', group_op=True) %}
126 |
127 |
128 | {% for sort_key, col_name in rules_columns %}
129 |
130 | {% if search_query %}
131 |
132 | {% else %}
133 |
134 | {% endif %}
135 | {{ col_name }}
136 |
137 |
138 |
139 | {% endfor %}
140 | Edit
141 | {% if group_op %}
142 |
143 |
144 |
145 | {% endif %}
146 |
147 |
148 | {% endmacro %}
149 |
150 | {% macro build_group_buttons_tfoot(button_colspan=10) %}
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | {% endmacro %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/actions.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% block title %}Flowspec Actions{% endblock %}
3 | {% block content %}
4 |
5 |
6 | Id
7 | Name
8 | Command
9 | Description
10 | Minimum level
11 | action
12 |
13 | {% for action in actions %}
14 |
15 | {{ action.id }}
16 | {{ action.name }}
17 | {{ action.command }}
18 |
19 | {{ action.description }}
20 |
21 |
22 | {{ action.role }}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {% endfor %}
34 |
35 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/api_key.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% block title %}ExaFS - ApiKeys{% endblock %}
3 | {% block content %}
4 |
Your machines and ApiKeys
5 |
6 |
7 | Machine address
8 | ApiKey
9 | Organization
10 | Expires
11 | Read only
12 | Action
13 |
14 | {% for row in keys %}
15 |
16 |
17 | {{ row.machine }}
18 |
19 |
20 | {{ row.key }}
21 |
22 |
23 | {{ row.org.name }}
24 |
25 | {{ row.expires|strftime }}
26 |
27 |
28 | {% if row.readonly %}
29 |
30 |
31 |
32 |
33 | {% endif %}
34 |
35 |
36 |
37 |
38 |
39 | {% if row.comment %}
40 |
41 |
42 |
43 | {% endif %}
44 |
45 |
46 | {% endfor %}
47 |
48 |
49 | Add new ApiKey
50 |
51 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/as_paths.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% block title %}AS Paths{% endblock %}
3 | {% block content %}
4 |
5 |
6 | Prefix
7 | AS-path
8 | Action
9 |
10 | {% for pth in paths %}
11 |
12 | {{ pth.prefix }}
13 |
14 | {{ pth.as_path }}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {% endfor %}
26 |
27 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/communities.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% block title %}Flowspec RTBH communities{% endblock %}
3 | {% block content %}
4 |
5 |
6 | Id
7 | Display Name
8 | Community
9 | Large comm.
10 | Extended comm.
11 | AS-path
12 | Description
13 | Minimum level
14 | Edit
15 |
16 | {% for community in communities %}
17 |
18 | {{ community.id }}
19 | {{ community.name }}
20 | {{ community.comm }}
21 | {{ community.larcomm }}
22 | {{ community.extcomm }}
23 | {{ community.as_path }}
24 |
25 | {{ community.description }}
26 |
27 |
28 | {{ community.role }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | {% endfor %}
40 |
41 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/dashboard_admin.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 |
3 |
4 | {% block title %}Flowspec{% endblock %}
5 | {% block content %}
6 |
7 | {% include 'pages/submenu_dashboard.html' %}
8 | {% if display_rules %}
9 |
21 |
22 | {% else %}
23 |
There are no {{ rstate|capitalize }} {{ table_title }}.
24 | {% endif %}
25 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/dashboard_search.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% from 'macros.html' import build_ip_tbody, build_rtbh_tbody, build_rules_thead %}
3 |
4 | {% block title %}Flowspec{% endblock %}
5 | {% block content %}
6 |
7 | {% include 'pages/submenu_dashboard.html' %}
8 |
9 |
10 |
11 | {{ build_rules_thead(rules_columns, rtype, rstate, sort_key, sort_order) }}
12 | {% if rtype_int == 1 %}
13 | {{ build_rtbh_tbody(rules, today, rtype_int) }}
14 | {% else %}
15 | {{ build_ip_tbody(rules, today, rtype_int) }}
16 | {% endif %}
17 |
18 |
19 |
20 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/dashboard_user.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% from 'macros.html' import build_ip_tbody, build_rtbh_tbody, build_rules_thead %}
3 |
4 |
5 | {% block title %}Flowspec{% endblock %}
6 | {% block content %}
7 |
8 | {% include 'pages/submenu_dashboard.html' %}
9 |
10 |
11 |
12 | {% if display_editable %}
13 |
{{ rstate|capitalize }} {{ table_title }} that you can modify
14 |
22 |
23 | {% else %}
24 |
There are no {{ rstate|capitalize }} {{ table_title }}.
25 | {% endif %}
26 |
27 | {% if display_readonly %}
28 |
{{ rstate|capitalize }} {{ table_title }} that are read-only for you
29 |
Those rules somehow including your network ranges. You can see them all for your information. However, you can not modify their expiration time or delete them.
30 |
31 | {{ dashboard_table_readonly_head }}
32 | {{ dashboard_table_readonly }}
33 |
34 | {% else %}
35 |
There are no read only {{ rstate }} {{ table_title }}.
36 | {% endif %}
37 |
38 |
40 |
41 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/dashboard_view.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% from 'macros.html' import build_ip_tbody, build_rtbh_tbody, build_rules_thead %}
3 |
4 |
5 | {% block title %}Flowspec{% endblock %}
6 | {% block content %}
7 |
8 | {% include 'pages/submenu_dashboard_view.html' %}
9 |
10 | {% if display_rules %}
11 |
{{ rstate|capitalize }} {{ table_title }}
12 |
You can see the rules for your information. However, you can not modify their expiration time or delete them.
13 |
14 | {{ dashboard_table_head }}
15 | {{ dashboard_table_body }}
16 |
17 |
18 | {% else %}
19 |
There are no {{ rstate|capitalize }} {{ table_title }}.
20 | {% endif %}
21 |
22 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/dashboard_whois.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 |
3 | {% block title %}Flowspec{% endblock %}
4 | {% block content %}
5 |
WHOIS for {{ ip_address }}
6 |
7 |
10 |
11 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/limit_reached.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% block title %}Rule limit reached{% endblock %}
3 | {% block content %}
4 |
5 |
6 |
You can't add new / reactivate {{ rule_type }} rule.
7 |
{{ message }}
8 |
9 |
10 | Rule type Current count Limit
11 |
12 |
13 | IPv4 (Flowspec4) {{ count_4 }} {{ org.limit_flowspec4|unlimited }}
14 |
15 |
16 | IPv6 {Flowspec6) {{ count_6 }} {{ org.limit_flowspec6|unlimited }}
17 |
18 |
19 | RTBH {{ count_rtbh }} {{ org.limit_rtbh|unlimited }}
20 |
21 |
22 |
Please delete some unnecesary rules, or contact system Administrator.
23 |
24 |
25 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/logout.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% block title %}Flowspec - logout{% endblock %}
3 | {% block content %}
4 |
Good Bye
5 |
Server time: {{ time }}
6 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/logs.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% block title %}Flowspec Users{% endblock %}
3 | {% block content %}
4 |
Commands log / latest on top
5 | {% if logs %}
6 |
7 | {% if logs.has_prev %}<< Newer logs {% else %}<< Newer logs{% endif %} |
8 | {% if logs.has_next %}Older logs >> {% else %}Older logs >>{% endif %}
9 |
10 |
11 |
12 | time
13 | task
14 | rule_type
15 | rule_id
16 | author
17 |
18 | {% for log in logs.items %}
19 |
20 | {{ log.time|strftime }}
21 | {{ log.task }}
22 | {{ log.rule_type }}
23 | {{ log.rule_id }}
24 | {{ log.author }}
25 |
26 | {% endfor %}
27 |
28 |
29 | {% if logs.has_prev %}<< Newer logs {% else %}<< Newer logs{% endif %} |
30 | {% if logs.has_next %}Older logs >> {% else %}Older logs >>{% endif %}
31 |
32 | {% else %}
33 |
No logs for last two days
34 | {% endif %}
35 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/machine_api_key.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% block title %}ExaFS - ApiKeys{% endblock %}
3 | {% block content %}
4 |
Machines and ApiKeys
5 |
6 | This is the list of all machines and their API keys, created by admin(s).
7 | In general, the keys should be Read Only and with expiration.
8 | If you need to create a full access Read/Write key, use usual user form with your organization settings.
9 |
10 |
11 |
12 | Machine address
13 | ApiKey
14 | Created by
15 | Created for
16 | Expires
17 | Read/Write ?
18 | Action
19 |
20 | {% for row in keys %}
21 |
22 |
23 | {{ row.machine }}
24 |
25 |
26 | {{ row.key }}
27 |
28 |
29 | {{ row.user.name }}
30 |
31 |
32 | {{ row.comment }}
33 |
34 | {{ row.expires|strftime }}
35 |
36 |
37 | {% if not row.readonly %}
38 |
39 |
40 |
41 |
42 | {% endif %}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {% endfor %}
51 |
52 |
53 | Add new Machine ApiKey
54 |
55 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/org_modal.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% block title %}ExaFS - choose our organization{% endblock %}
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
11 |
12 |
You are a member of more than one organisation. Please select an organization for this session, due to the rules counting towards the organization limit.
13 |
14 |
20 |
21 |
22 |
23 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/orgs.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% block title %}Flowspec Organizations{% endblock %}
3 | {% block content %}
4 |
5 |
6 | RTBH All Count
7 | Flowspec4 All Count
8 | Flowspec6 All Count
9 |
10 |
11 | {{ rtbh_all_count }} / {{ rtbh_limit }}
12 | {{ flowspec4_all_count }} / {{ flowspec_limit }}
13 | {{ flowspec6_all_count }} / {{ flowspec_limit }}
14 |
15 |
16 |
17 |
18 |
19 |
20 | Name
21 | Limit for rules
22 | Adress Ranges
23 | action
24 |
25 | {% for org in orgs %}
26 |
27 | {{ org.name }}
28 |
29 | IPv4: {{ org.limit_flowspec4 | unlimited }} / {{ flowspec4_counts[org.id] | default(0) }}
30 | IPv6: {{ org.limit_flowspec6 | unlimited }} / {{ flowspec6_counts[org.id] | default(0) }}
31 | RTBH: {{ org.limit_rtbh | unlimited }} / {{ rtbh_counts[org.id] | default(0) }}
32 |
33 |
34 | {% set rows = org.arange.split() %}
35 |
36 | {% for row in rows %}
37 | {{ row }}
38 | {% endfor %}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {% endfor %}
51 |
52 |
53 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/submenu_dashboard.html:
--------------------------------------------------------------------------------
1 |
2 | {% block submenu_dashboard %}
3 |
4 |
5 |
{{ rstate|capitalize }} {{ table_title }}
6 |
7 |
21 |
22 |
23 |
24 |
52 |
53 |
54 | Active
55 |
56 |
57 | Expired
58 |
59 |
60 | All
61 |
62 |
63 |
64 |
65 |
66 |
67 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/submenu_dashboard_view.html:
--------------------------------------------------------------------------------
1 |
2 | {% block submenu_dashboard %}
3 |
4 |
{{ rstate|capitalize }} {{ table_title }}
5 |
6 |
7 |
56 |
57 |
58 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/user_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% block title %}Flowspec Actions{% endblock %}
3 | {% block content %}
4 |
Updated {{ updated}} records.
5 |
Users with multiple organizations
6 |
Records of users with multilple orgs could not be updated.
7 | {% for user, orgs in users.items() %}
8 |
9 | {{ user }}
10 |
11 | {% for org in orgs %}
12 | {{ org }}
13 | {% endfor %}
14 |
15 |
16 | {% endfor %}
17 |
18 |
19 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/templates/pages/users.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/default.html' %}
2 | {% block title %}Flowspec Users{% endblock %}
3 | {% block content %}
4 |
5 |
6 | UUID
7 | Name
8 | Email
9 | Phone
10 | Notice
11 | Role(s)
12 | Organization(s)
13 | Action
14 |
15 | {% for user in users %}
16 |
17 | {{ user.uuid }}
18 | {{ user.name if user.name }}
19 | {{ user.email if user.name }}
20 | {{ user.phone if user.name }}
21 | {{ user.comment if user.comment }}
22 |
23 | {% for role in user.role %}
24 | {{ role.name }}
25 | {% endfor %}
26 |
27 |
28 | {% for org in user.organization %}
29 | {{ org.name }}
30 | {% endfor %}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | {% endfor %}
42 |
43 | {% endblock %}
--------------------------------------------------------------------------------
/flowapp/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CESNET/exafs/866f3bab1b207fccc6118efeaa4281a243a36e1b/flowapp/tests/__init__.py
--------------------------------------------------------------------------------
/flowapp/tests/conftest.py:
--------------------------------------------------------------------------------
1 | """
2 | PyTest configuration file for all tests
3 | """
4 |
5 | import os
6 | import json
7 | import pytest
8 | from sqlalchemy import create_engine
9 | from sqlalchemy.orm import sessionmaker
10 |
11 | from flowapp import create_app
12 | from flowapp import db as _db
13 | from datetime import datetime
14 | import flowapp.models
15 |
16 |
17 | TESTDB = "test_project.db"
18 | TESTDB_PATH = "/tmp/{}".format(TESTDB)
19 | TEST_DATABASE_URI = "sqlite:///" + TESTDB_PATH
20 |
21 |
22 | class FieldMock:
23 | def __init__(self):
24 | self.data = None
25 | self.errors = []
26 |
27 |
28 | class RuleMock:
29 | def __init__(self):
30 | self.source = None
31 | self.source_mask = None
32 | self.dest = None
33 | self.dest_mask = None
34 |
35 |
36 | @pytest.fixture
37 | def field():
38 | return FieldMock()
39 |
40 |
41 | @pytest.fixture
42 | def field_class():
43 | return FieldMock
44 |
45 |
46 | @pytest.fixture
47 | def rule():
48 | return RuleMock()
49 |
50 |
51 | @pytest.fixture(scope="session")
52 | def app(request):
53 | """
54 | Create a Flask app, and override settings, for the whole test session.
55 | """
56 |
57 | _app = create_app()
58 |
59 | _app.config.update(
60 | EXA_API="HTTP",
61 | EXA_API_URL="http://localhost:5000/",
62 | TESTING=True,
63 | SQLALCHEMY_DATABASE_URI=TEST_DATABASE_URI,
64 | SQLALCHEMY_TRACK_MODIFICATIONS=False,
65 | JWT_SECRET="testing",
66 | API_KEY="testkey",
67 | SECRET_KEY="testkeysession",
68 | LOCAL_USER_UUID="jiri.vrany@cesnet.cz",
69 | LOCAL_AUTH=True,
70 | )
71 |
72 | print("\n----- CREATE FLASK APPLICATION\n")
73 | context = _app.app_context()
74 | context.push()
75 | yield _app
76 | print("\n----- CREATE FLASK APPLICATION CONTEXT\n")
77 |
78 | context.pop()
79 | print("\n----- RELEASE FLASK APPLICATION CONTEXT\n")
80 |
81 |
82 | @pytest.fixture(scope="session")
83 | def client(app, request):
84 | """
85 | Get the test_client from the app, for the whole test session.
86 | """
87 | print("\n----- CREATE FLASK TEST CLIENT\n")
88 | return app.test_client()
89 |
90 |
91 | @pytest.fixture(scope="session")
92 | def db(app, request):
93 | """
94 | Create entire database for every test.
95 | """
96 | engine = create_engine(app.config["SQLALCHEMY_DATABASE_URI"], echo=True)
97 | sessionmaker(bind=engine)
98 | print("\n----- CREATE TEST DB CONNECTION POOL\n")
99 | if os.path.exists(TESTDB_PATH):
100 | os.unlink(TESTDB_PATH)
101 |
102 | with app.app_context():
103 | _db.init_app(app)
104 | print("#: cleaning database")
105 | _db.reflect()
106 | _db.drop_all()
107 | print("#: creating tables")
108 | _db.create_all()
109 |
110 | users = [
111 | {"name": "jiri.vrany@cesnet.cz", "role_id": 3, "org_id": 1},
112 | {"name": "petr.adamec@cesnet.cz", "role_id": 3, "org_id": 1},
113 | ]
114 | print("#: inserting users")
115 | flowapp.models.insert_users(users)
116 |
117 | def teardown():
118 | _db.session.commit()
119 | _db.drop_all()
120 | os.unlink(TESTDB_PATH)
121 |
122 | request.addfinalizer(teardown)
123 | return _db
124 |
125 |
126 | @pytest.fixture(scope="session")
127 | def jwt_token(client, app, db, request):
128 | """
129 | Get the test_client from the app, for the whole test session.
130 | """
131 | mkey = "testkey"
132 |
133 | with app.app_context():
134 | model = flowapp.models.ApiKey(machine="127.0.0.1", key=mkey, user_id=1, org_id=1)
135 | db.session.add(model)
136 | db.session.commit()
137 |
138 | print("\n----- GET JWT TEST TOKEN\n")
139 | url = "/api/v3/auth"
140 | headers = {"x-api-key": mkey}
141 | token = client.get(url, headers=headers)
142 | data = json.loads(token.data)
143 | return data["token"]
144 |
145 |
146 | @pytest.fixture(scope="session")
147 | def expired_auth_token(client, app, db, request):
148 | """
149 | Get the test_client from the app, for the whole test session.
150 | """
151 | test_key = "expired_test_key"
152 | expired_date = datetime.strptime("2019-01-01", "%Y-%m-%d")
153 | with app.app_context():
154 | model = flowapp.models.ApiKey(machine="127.0.0.1", key=test_key, user_id=1, expires=expired_date, org_id=1)
155 | db.session.add(model)
156 | db.session.commit()
157 |
158 | return test_key
159 |
160 |
161 | @pytest.fixture(scope="session")
162 | def readonly_jwt_token(client, app, db, request):
163 | """
164 | Get the test_client from the app, for the whole test session.
165 | """
166 | readonly_key = "readonly-testkey"
167 | with app.app_context():
168 | model = flowapp.models.ApiKey(machine="127.0.0.1", key=readonly_key, user_id=1, readonly=True, org_id=1)
169 | db.session.add(model)
170 | db.session.commit()
171 |
172 | print("\n----- GET JWT TEST TOKEN\n")
173 | url = "/api/v3/auth"
174 | headers = {"x-api-key": readonly_key}
175 | token = client.get(url, headers=headers)
176 | data = json.loads(token.data)
177 | return data["token"]
178 |
179 |
180 | @pytest.fixture(scope="session")
181 | def auth_client(client):
182 | """
183 | Get the test_client from the app, for the whole test session.
184 | """
185 | print("\n----- CREATE AUTHENTICATED FLASK TEST CLIENT\n")
186 | client.get("/local-login")
187 | return client
188 |
--------------------------------------------------------------------------------
/flowapp/tests/test_api_auth.py:
--------------------------------------------------------------------------------
1 | # Test for api authorization
2 | import json
3 |
4 |
5 | def test_token(client, jwt_token):
6 | """
7 | test that token authorization works
8 | """
9 | req = client.get("/api/v3/test_token", headers={"x-access-token": jwt_token})
10 |
11 | assert req.status_code == 200
12 |
13 |
14 | def test_expired_token(client, expired_auth_token):
15 | """
16 | test that expired token authorization return 401
17 | """
18 | req = client.get("/api/v3/auth", headers={"x-api-key": expired_auth_token})
19 |
20 | assert req.status_code == 401
21 |
22 |
23 | def test_withnout_token(client):
24 | """
25 | test that without token authorization return 401
26 | """
27 | req = client.get("/api/v3/test_token")
28 |
29 | assert req.status_code == 401
30 |
31 |
32 | def test_readonly_token(client, readonly_jwt_token):
33 | """
34 | test that readonly flag is set correctly
35 | """
36 | req = client.get("/api/v3/test_token", headers={"x-access-token": readonly_jwt_token})
37 |
38 | assert req.status_code == 200
39 | data = json.loads(req.data)
40 | assert data['readonly']
41 |
42 |
43 | def test_readonly_token_ipv4_create(client, db, readonly_jwt_token):
44 | """
45 | test that readonly token can't create ipv4 rule
46 | """
47 | headers = {"x-access-token": readonly_jwt_token}
48 |
49 | req = client.post(
50 | "/api/v3/rules/ipv4",
51 | headers=headers,
52 | json={
53 | "action": 2,
54 | "protocol": "tcp",
55 | "source": "147.230.17.117",
56 | "source_mask": 32,
57 | "source_port": "",
58 | "expires": "1444913400",
59 | },
60 | )
61 |
62 | assert req.status_code == 403
63 |
--------------------------------------------------------------------------------
/flowapp/tests/test_api_deprecated.py:
--------------------------------------------------------------------------------
1 | V_PREFIX = "/api/v1"
2 |
3 |
4 | def test_token(client, jwt_token):
5 | """
6 | test that token authorization works
7 | """
8 | req = client.get(f"{V_PREFIX}/test_token", headers={"x-access-token": jwt_token})
9 |
10 | assert req.status_code == 400
11 |
12 |
13 | def test_withnout_token(client):
14 | """
15 | test that without token authorization return 401
16 | """
17 | req = client.get(f"{V_PREFIX}/test_token")
18 |
19 | assert req.status_code == 400
20 |
21 |
22 | def test_rules(client, db, jwt_token):
23 | """
24 | test that there is one ipv4 rule created in the first test
25 | """
26 | req = client.get(f"{V_PREFIX}/rules", headers={"x-access-token": jwt_token})
27 |
28 | assert req.status_code == 400
29 |
--------------------------------------------------------------------------------
/flowapp/tests/test_flowapp.py:
--------------------------------------------------------------------------------
1 | def test_dashboard_not_auth(client):
2 |
3 | response = client.get("/dashboard/ipv4/active/?sort=expires&order=desc")
4 |
5 | # Expecting a 302 redirect to login
6 | assert response.status_code == 302
7 |
8 |
9 | def test_dashboard(auth_client):
10 |
11 | response = auth_client.get("/dashboard/ipv4/active/?sort=expires&order=desc")
12 |
13 | # Check that the request is successful and renders the correct template
14 | assert response.status_code == 200 # Expecting a 200 OK if the user is authenticated
15 |
--------------------------------------------------------------------------------
/flowapp/tests/test_flowspec.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import flowapp.flowspec
3 |
4 |
5 | def test_translate_number():
6 | """
7 | tests for x (integer) to =x
8 | """
9 | assert "[=10]" == flowapp.flowspec.translate_sequence("10")
10 |
11 |
12 | def test_raises():
13 | """
14 | tests for translator
15 | """
16 | with pytest.raises(ValueError):
17 | flowapp.flowspec.translate_sequence("ahoj")
18 |
19 |
20 | def test_raises_bad_number():
21 | """
22 | tests for translator
23 | """
24 | with pytest.raises(ValueError):
25 | flowapp.flowspec.translate_sequence("75555")
26 |
27 |
28 | def test_translate_range():
29 | """
30 | tests for x-y to >=x&<=y
31 | """
32 | assert "[>=10&<=20]" == flowapp.flowspec.translate_sequence("10-20")
33 |
34 |
35 | def test_exact_rule():
36 | """
37 | test for >=x&<=y to >=x&<=y
38 | """
39 | assert "[>=10&<=20]" == flowapp.flowspec.translate_sequence(">=10&<=20")
40 |
41 |
42 | def test_greater_than():
43 | """
44 | test for >x to >=x&<=65535
45 | """
46 | assert "[>=10&<=65535]" == flowapp.flowspec.translate_sequence(">10")
47 |
48 |
49 | def test_greater_equal_than():
50 | """
51 | test for >=x to >=x&<=65535
52 | """
53 | assert "[>=10&<=65535]" == flowapp.flowspec.translate_sequence(">=10")
54 |
55 |
56 | def test_lower_than():
57 | """
58 | test for
=0&<=0
59 | """
60 | assert "[>=0&<=10]" == flowapp.flowspec.translate_sequence("<10")
61 |
62 |
63 | def test_lower_equal_than():
64 | """
65 | test for =0&<=0
66 | """
67 | assert "[>=0&<=10]" == flowapp.flowspec.translate_sequence("<=10")
68 |
--------------------------------------------------------------------------------
/flowapp/tests/test_forms.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from flask import Flask
3 | import flowapp.forms
4 |
5 |
6 | @pytest.fixture()
7 | def app():
8 | app = Flask(__name__)
9 | app.secret_key = "test"
10 | return app
11 |
12 |
13 | @pytest.fixture()
14 | def ip_form(app, field_class):
15 | with app.test_request_context(): # Push the request context
16 | form = flowapp.forms.IPForm()
17 | form.source = field_class()
18 | form.dest = field_class()
19 | form.source_mask = field_class()
20 | form.dest_mask = field_class()
21 | return form
22 |
23 |
24 | def test_ip_form_created(ip_form):
25 | assert ip_form.source.data is None
26 | assert ip_form.source.errors == []
27 |
28 |
29 | @pytest.mark.parametrize(
30 | "address, mask, expected",
31 | [
32 | ("147.230.23.25", "24", False),
33 | ("147.230.23.0", "24", True),
34 | ("0.0.0.0", "0", True),
35 | ("2001:718:1C01:1111::1111", "64", False),
36 | ("2001:718:1C01:1111::", "64", True),
37 | ],
38 | )
39 | def test_ip_form_validate_source_address(ip_form, address, mask, expected):
40 | ip_form.source.data = address
41 | ip_form.source_mask.data = mask
42 | assert ip_form.validate_source_address() == expected
43 |
44 |
45 | @pytest.mark.parametrize(
46 | "address, mask, expected",
47 | [
48 | ("147.230.23.25", "24", False),
49 | ("147.230.23.0", "24", True),
50 | ("0.0.0.0", "0", True),
51 | ("2001:718:1C01:1111::1111", "64", False),
52 | ("2001:718:1C01:1111::", "64", True),
53 | ],
54 | )
55 | def test_ip_form_validate_dest_address(ip_form, address, mask, expected):
56 | ip_form.dest.data = address
57 | ip_form.dest_mask.data = mask
58 | assert ip_form.validate_dest_address() == expected
59 |
60 |
61 | @pytest.mark.parametrize(
62 | "address, mask, ranges, expected",
63 | [
64 | ("147.230.23.0", "24", ["147.230.0.0/16", "2001:718:1c01::/48"], True),
65 | ("0.0.0.0", "0", ["147.230.0.0/16", "2001:718:1c01::/48"], False),
66 | ("195.113.0.0", "16", ["195.113.0.0/18", "195.113.64.0/21"], False),
67 | ],
68 | )
69 | def test_ip_form_validate_address_mask(ip_form, address, mask, ranges, expected):
70 | ip_form.net_ranges = ranges
71 | ip_form.source.data = address
72 | ip_form.source_mask.data = mask
73 | assert ip_form.validate_address_ranges() == expected
74 |
--------------------------------------------------------------------------------
/flowapp/tests/test_login.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CESNET/exafs/866f3bab1b207fccc6118efeaa4281a243a36e1b/flowapp/tests/test_login.py
--------------------------------------------------------------------------------
/flowapp/tests/test_models.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | import flowapp.models as models
4 |
5 |
6 | def test_insert_ipv4(db):
7 | """
8 | test the record can be inserted
9 | :param db: conftest fixture
10 | :return:
11 | """
12 | model = models.Flowspec4(
13 | source="192.168.1.1",
14 | source_mask="32",
15 | source_port="80",
16 | destination="",
17 | destination_mask="",
18 | destination_port="",
19 | protocol="tcp",
20 | flags="",
21 | packet_len="",
22 | fragment="",
23 | action_id=1,
24 | expires=datetime.now(),
25 | user_id=1,
26 | org_id=1,
27 | rstate_id=1,
28 | )
29 | db.session.add(model)
30 | db.session.commit()
31 |
32 |
33 | def test_get_ipv4_model_if_exists(db):
34 | """
35 | test if the function find existing model correctly
36 | :param db: conftest fixture
37 | :return:
38 | """
39 | model = models.Flowspec4(
40 | source="192.168.1.1",
41 | source_mask="32",
42 | source_port="80",
43 | destination="",
44 | destination_mask="",
45 | destination_port="",
46 | protocol="tcp",
47 | flags="",
48 | fragment="",
49 | packet_len="",
50 | action_id=1,
51 | expires=datetime.now(),
52 | user_id=1,
53 | org_id=1,
54 | rstate_id=1,
55 | )
56 | db.session.add(model)
57 | db.session.commit()
58 |
59 | form_data = {
60 | "source": "192.168.1.1",
61 | "source_mask": "32",
62 | "source_port": "80",
63 | "dest": "",
64 | "dest_mask": "",
65 | "dest_port": "",
66 | "protocol": "tcp",
67 | "flags": "",
68 | "packet_len": "",
69 | "action": 1,
70 | }
71 |
72 | result = models.get_ipv4_model_if_exists(form_data, 1)
73 | assert result
74 | assert result == model
75 |
76 |
77 | def test_get_ipv6_model_if_exists(db):
78 | """
79 | test if the function find existing model correctly
80 | :param db: conftest fixture
81 | :return:
82 | """
83 | model = models.Flowspec6(
84 | source="2001:0db8:85a3:0000:0000:8a2e:0370:7334",
85 | source_mask="32",
86 | source_port="80",
87 | destination="",
88 | destination_mask="",
89 | destination_port="",
90 | next_header="tcp",
91 | flags="",
92 | packet_len="",
93 | action_id=1,
94 | expires=datetime.now(),
95 | user_id=1,
96 | org_id=1,
97 | rstate_id=1,
98 | )
99 | db.session.add(model)
100 | db.session.commit()
101 |
102 | form_data = {
103 | "source": "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
104 | "source_mask": "32",
105 | "source_port": "80",
106 | "dest": "",
107 | "dest_mask": "",
108 | "dest_port": "",
109 | "next_header": "tcp",
110 | "flags": "",
111 | "packet_len": "",
112 | "action": 1,
113 | }
114 |
115 | result = models.get_ipv6_model_if_exists(form_data, 1)
116 | assert result
117 | assert result == model
118 |
119 |
120 | def test_ipv4_eq(db):
121 | """
122 | test that creating with valid data returns 201
123 | """
124 | model_A = models.Flowspec4(
125 | source="192.168.1.1",
126 | source_mask="32",
127 | source_port="80",
128 | destination="",
129 | destination_mask="",
130 | destination_port="",
131 | protocol="tcp",
132 | flags="",
133 | fragment="",
134 | packet_len="",
135 | action_id=1,
136 | expires="123",
137 | user_id=1,
138 | org_id=1,
139 | rstate_id=1,
140 | )
141 |
142 | model_B = models.Flowspec4(
143 | source="192.168.1.1",
144 | source_mask="32",
145 | source_port="80",
146 | destination="",
147 | destination_mask="",
148 | destination_port="",
149 | protocol="tcp",
150 | flags="",
151 | fragment="",
152 | packet_len="",
153 | action_id=1,
154 | expires="123456",
155 | user_id=1,
156 | org_id=1,
157 | rstate_id=1,
158 | )
159 |
160 | assert model_A == model_B
161 |
162 |
163 | def test_ipv4_ne(db):
164 | """
165 | test that creating with valid data returns 201
166 | """
167 | model_A = models.Flowspec4(
168 | source="192.168.2.2",
169 | source_mask="32",
170 | source_port="80",
171 | destination="",
172 | destination_mask="",
173 | destination_port="",
174 | protocol="tcp",
175 | flags="",
176 | fragment="",
177 | packet_len="",
178 | action_id=1,
179 | expires="123",
180 | user_id=1,
181 | org_id=1,
182 | rstate_id=1,
183 | )
184 |
185 | model_B = models.Flowspec4(
186 | source="192.168.1.1",
187 | source_mask="32",
188 | source_port="80",
189 | destination="",
190 | destination_mask="",
191 | destination_port="",
192 | protocol="tcp",
193 | flags="",
194 | fragment="",
195 | packet_len="",
196 | action_id=1,
197 | expires="123456",
198 | user_id=1,
199 | org_id=1,
200 | rstate_id=1,
201 | )
202 |
203 | assert model_A != model_B
204 |
205 |
206 | def test_rtbj_eq(db):
207 | """
208 | test that two equal rtbh rules are equal
209 | """
210 | model_A = models.RTBH(
211 | ipv4="192.168.1.1",
212 | ipv4_mask="32",
213 | ipv6="",
214 | ipv6_mask="",
215 | community_id=1,
216 | expires="123",
217 | user_id=1,
218 | org_id=1,
219 | rstate_id=1,
220 | )
221 |
222 | model_B = models.RTBH(
223 | ipv4="192.168.1.1",
224 | ipv4_mask="32",
225 | ipv6="",
226 | ipv6_mask="",
227 | community_id=1,
228 | expires="123456",
229 | user_id=1,
230 | org_id=1,
231 | rstate_id=1,
232 | )
233 |
234 | assert model_A == model_B
235 |
--------------------------------------------------------------------------------
/flowapp/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import ipaddress
2 | import pytest
3 |
4 | from datetime import datetime, timedelta
5 |
6 | from flowapp import utils
7 |
8 |
9 | @pytest.mark.parametrize(
10 | "apitime, preformat",
11 | [
12 | ("10/15/2015 14:46", "us"),
13 | ("2015/10/15 14:46", "yearfirst"),
14 | ("1444913400", "timestamp"),
15 | (1444913400, "timestamp"),
16 | ],
17 | )
18 | def test_parse_api_time(apitime, preformat):
19 | """
20 | is the time parsed correctly
21 | """
22 | result = utils.parse_api_time(apitime)
23 | assert isinstance(result, tuple)
24 | assert result[0] == datetime(2015, 10, 15, 14, 50)
25 | assert result[1] == preformat
26 |
27 |
28 | @pytest.mark.parametrize(
29 | "apitime", ["10/152015 14:46", "201/10/15 14:46", "144123254913400", "abcd"]
30 | )
31 | def test_parse_api_time_bad_time(apitime):
32 | """
33 | is the time parsed correctly
34 | """
35 | assert not utils.parse_api_time(apitime)
36 |
37 |
38 | def test_get_rule_state_by_time():
39 | """
40 | Test if time in the past returns 2
41 | """
42 | past = datetime.now() - timedelta(days=1)
43 |
44 | assert utils.get_state_by_time(past) == 2
45 |
46 |
47 | def test_round_to_ten():
48 | """
49 | Test if the time is rounded correctly
50 | """
51 | d1 = datetime(2013, 9, 2, 16, 25, 59)
52 | d2 = datetime(2013, 9, 2, 16, 32, 59)
53 | dround = datetime(2013, 9, 2, 16, 30, 00)
54 |
55 | assert utils.round_to_ten_minutes(d1) == dround
56 | assert utils.round_to_ten_minutes(d2) == dround
57 |
58 |
59 | @pytest.mark.parametrize(
60 | "address_a, address_b",
61 | [
62 | (
63 | "2001:718:1c01:16:f1c4:c682:817:7e23",
64 | "2001:0718:1c01:0016:f1c4:c682:0817:7e23",
65 | ),
66 | ("2001:718::", "2001:718::0"),
67 | ("2001:718::0", "2001:0718:0000:0000:0000:0000:0000:0000"),
68 | ],
69 | )
70 | def test_ipv6_comparsion(address_a, address_b):
71 | assert ipaddress.ip_address(address_a) == ipaddress.ip_address(address_b)
72 |
--------------------------------------------------------------------------------
/flowapp/tests/test_validators.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import flowapp.validators
3 |
4 |
5 | def test_port_string_len_raises(field):
6 | port = flowapp.validators.PortString()
7 | field.data = "1;2;3;4;5;6;7;8"
8 | with pytest.raises(flowapp.validators.ValidationError):
9 | port(None, field)
10 |
11 |
12 | @pytest.mark.parametrize(
13 | "address, mask, expected",
14 | [
15 | ("147.230.23.25", "24", False),
16 | ("147.230.23.0", "24", True),
17 | ("0.0.0.0", "0", True),
18 | ("2001:718:1C01:1111::1111", "64", False),
19 | ("2001:718:1C01:1111::", "64", True),
20 | ],
21 | )
22 | def test_is_valid_address_with_mask(address, mask, expected):
23 | assert flowapp.validators.address_with_mask(address, mask) == expected
24 |
25 |
26 | @pytest.mark.parametrize("address", ["147.230.23.25", "147.230.23.0"])
27 | def test_ip4address_passes(field, address):
28 | adr = flowapp.validators.IPv4Address()
29 | field.data = address
30 | adr(None, field)
31 |
32 |
33 | @pytest.mark.parametrize(
34 | "address",
35 | [
36 | "2001:718:1C01:1111::1111",
37 | "2001:718:1C01:1111::",
38 | ],
39 | )
40 | def test_ip6address_passes(field, address):
41 | adr = flowapp.validators.IPv6Address()
42 | field.data = address
43 | adr(None, field)
44 |
45 |
46 | @pytest.mark.parametrize(
47 | "address",
48 | [
49 | "2001:718:1C01:1111::1111",
50 | "2001:718:1C01:1111::",
51 | ],
52 | )
53 | def test_bad_ip6address_raises(field, address):
54 | adr = flowapp.validators.IPv4Address()
55 | field.data = address
56 | with pytest.raises(flowapp.validators.ValidationError):
57 | adr(None, field)
58 |
59 |
60 | @pytest.mark.parametrize(
61 | "expired", ["2018/10/25 14:46", "2018/12/20 9:46", "2019/05/22 12:33"]
62 | )
63 | def test_expired_date_raises(field, expired):
64 | adr = flowapp.validators.DateNotExpired()
65 | field.data = expired
66 | with pytest.raises(flowapp.validators.ValidationError):
67 | adr(None, field)
68 |
69 |
70 | @pytest.mark.parametrize(
71 | "address",
72 | [
73 | "147.230.1000.25",
74 | "2001:718::::",
75 | ],
76 | )
77 | def test_ipaddress_raises(field, address):
78 | adr = flowapp.validators.IPv6Address()
79 | field.data = address
80 | with pytest.raises(flowapp.validators.ValidationError):
81 | adr(None, field)
82 |
83 |
84 | @pytest.mark.parametrize(
85 | "address, mask, ranges, expected",
86 | [
87 | ("147.230.23.0", "24", ["147.230.0.0/16", "147.251.0.0/16"], True),
88 | ("147.230.23.0", "24", ["147.230.0.0/16", "147.251.0.0/16"], True),
89 | ],
90 | )
91 | def test_editable_rule(rule, address, mask, ranges, expected):
92 | rule.source = address
93 | rule.source_mask = mask
94 | assert flowapp.validators.editable_range(rule, ranges) == expected
95 |
96 |
97 | @pytest.mark.parametrize(
98 | "address, mask, ranges, expected",
99 | [
100 | ("147.230.23.0", "24", ["147.230.0.0/16", "147.251.0.0/16"], True),
101 | ("147.233.23.0", "24", ["147.230.0.0/16", "147.251.0.0/16"], False),
102 | ("147.230.23.0", "24", ["147.230.0.0/16", "2001:718:1c01::/48"], True),
103 | ("195.113.0.0", "16", ["0.0.0.0/0", "::/0"], True),
104 | ],
105 | )
106 | def test_address_in_range(address, mask, ranges, expected):
107 | assert flowapp.validators.address_in_range(address, ranges) == expected
108 |
109 |
110 | @pytest.mark.parametrize(
111 | "address, mask, ranges, expected",
112 | [
113 | ("147.230.23.0", "24", ["147.230.0.0/16", "147.251.0.0/16"], True),
114 | ("147.233.23.0", "24", ["147.230.0.0/16", "147.251.0.0/16"], False),
115 | ("195.113.0.0", "16", ["195.113.0.0/18", "195.113.64.0/21"], False),
116 | ("195.113.0.0", "16", ["0.0.0.0/0", "::/0"], True),
117 | (
118 | "195.113.0.0",
119 | "16",
120 | ["147.230.0.0/16", "2001:718:1c01::/48", "0.0.0.0/0", "::/0"],
121 | True,
122 | ),
123 | ],
124 | )
125 | def test_network_in_range(address, mask, ranges, expected):
126 | assert flowapp.validators.network_in_range(address, mask, ranges) == expected
127 |
128 |
129 | @pytest.mark.parametrize(
130 | "address, mask, ranges, expected",
131 | [
132 | ("195.113.0.0", "16", ["147.230.0.0/16", "195.113.250.0/24"], True),
133 | ],
134 | )
135 | def test_range_in_network(address, mask, ranges, expected):
136 | assert flowapp.validators.range_in_network(address, mask, ranges) == expected
137 |
--------------------------------------------------------------------------------
/flowapp/utils.py:
--------------------------------------------------------------------------------
1 | from operator import ge, lt
2 | from datetime import datetime, timedelta
3 | from flask import flash
4 | from flowapp.constants import (
5 | COMP_FUNCS,
6 | TIME_YEAR,
7 | TIME_US,
8 | TIME_STMP,
9 | TIME_FORMAT_ARG,
10 | RULE_TYPES_DICT,
11 | FORM_TIME_PATTERN,
12 | )
13 |
14 |
15 | def other_rtypes(rtype):
16 | """
17 | get rtype and return list of remaining rtypes
18 | for example get ipv4 and return [ipv6, rtbh]
19 | """
20 | result = list(RULE_TYPES_DICT.keys())
21 | try:
22 | result.remove(rtype)
23 | except ValueError:
24 | pass
25 |
26 | return result
27 |
28 |
29 | def output_date_format(json_request_data, pref_format=TIME_YEAR):
30 | """
31 | prefer user setting from parameter, if the parameter is not set
32 | then use the prefered format computed from input date
33 | """
34 | if not json_request_data:
35 | return pref_format
36 |
37 | if TIME_FORMAT_ARG in json_request_data and json_request_data[TIME_FORMAT_ARG]:
38 | return json_request_data[TIME_FORMAT_ARG]
39 | else:
40 | return pref_format
41 |
42 |
43 | def parse_api_time(apitime):
44 | """
45 | check if the api time is in US, EU or timestamp format
46 | :param apitime: string with date and time
47 | :returns: datetime, prefered format
48 | """
49 |
50 | apitime = str(apitime)
51 | try:
52 | return (
53 | round_to_ten_minutes(datetime.strptime(apitime, FORM_TIME_PATTERN)),
54 | TIME_US,
55 | )
56 | except ValueError:
57 | mytime = False
58 |
59 | try:
60 | return round_to_ten_minutes(webpicker_to_datetime(apitime)), TIME_YEAR
61 | except ValueError:
62 | mytime = False
63 |
64 | try:
65 | return round_to_ten_minutes(webpicker_to_datetime(apitime, TIME_US)), TIME_US
66 | except ValueError:
67 | mytime = False
68 |
69 | try:
70 | return round_to_ten_minutes(datetime.fromtimestamp(int(apitime))), TIME_STMP
71 | except OverflowError:
72 | mytime = False
73 | except ValueError:
74 | mytime = False
75 |
76 | return False
77 |
78 |
79 | def quote_to_ent(comment):
80 | """
81 | Convert all " to "
82 | Used for comment sanitize / because of tooltip in dashboard break when quotes are unescaped
83 | :param comment: string to be sanitized
84 | :return: string
85 | """
86 | if comment:
87 | return comment.replace('"', """)
88 |
89 |
90 | def webpicker_to_datetime(webtime, format=TIME_YEAR):
91 | """
92 | convert 'YYYY/MM/DD HH:mm' to datetime
93 | """
94 | if format == TIME_YEAR:
95 | formating_string = "%Y/%m/%d %H:%M"
96 | else:
97 | formating_string = "%m/%d/%Y %H:%M"
98 |
99 | return datetime.strptime(webtime, formating_string)
100 |
101 |
102 | def datetime_to_webpicker(python_time, format=TIME_YEAR):
103 | """
104 | convert datetime to 'YYYY/MM/DD HH:mm' string
105 | """
106 | if format == TIME_YEAR:
107 | formating_string = "%Y/%m/%d %H:%M"
108 | else:
109 | formating_string = "%m/%d/%Y %H:%M"
110 |
111 | return datetime.strftime(python_time, formating_string)
112 |
113 |
114 | def get_state_by_time(python_time):
115 | """
116 | returns state for rule based on given time
117 | if given time is in the past returns 2 (withdrawed rule)
118 | else returns 1
119 | :param python_time:
120 | :return: integer rstate
121 | """
122 | present = datetime.now()
123 |
124 | if python_time <= present:
125 | return 2
126 | else:
127 | return 1
128 |
129 |
130 | def round_to_ten_minutes(python_time):
131 | """
132 | Round given time to nearest ten minutes
133 | :param python_time: datetime
134 | :return: datetime
135 | """
136 | python_time += timedelta(minutes=5)
137 | python_time -= timedelta(
138 | minutes=python_time.minute % 10,
139 | seconds=python_time.second,
140 | microseconds=python_time.microsecond,
141 | )
142 |
143 | return python_time
144 |
145 |
146 | def flash_errors(form):
147 | """
148 | Flash all error messages
149 | :param form: WTForm object
150 | :return: none
151 | """
152 | for field, errors in form.errors.items():
153 | for error in errors:
154 | flash("Error in the %s field - %s" % (getattr(form, field).label.text, error))
155 |
156 |
157 | def active_css_rstate(rtype, rstate):
158 | """
159 | returns dict with rstates as keys and css class value
160 | :param rstate: string
161 | :return: dict
162 | """
163 |
164 | return {
165 | "active": "",
166 | "expired": "",
167 | "all": "",
168 | "ipv4": "",
169 | "ipv6": "",
170 | "rtbh": "",
171 | rtype: "active",
172 | rstate: "active",
173 | }
174 |
175 |
176 | def get_comp_func(rstate="active"):
177 | try:
178 | comp_func = COMP_FUNCS[rstate]
179 | except IndexError:
180 | comp_func = None
181 | except KeyError:
182 | comp_func = None
183 |
184 | return comp_func
185 |
--------------------------------------------------------------------------------
/flowapp/validators.py:
--------------------------------------------------------------------------------
1 | import ipaddress
2 | from datetime import datetime
3 | from wtforms.validators import ValidationError
4 |
5 | from flowapp import constants, flowspec, utils
6 |
7 |
8 | def filter_rules_in_network(net_ranges, rules):
9 | """
10 | Return only rules matching user net ranges
11 | :param net_ranges: list of network ranges
12 | :param rules: list of rules (ipv4 or ipv6
13 | :return: filtered list of rules
14 | """
15 | return [
16 | rule
17 | for rule in rules
18 | if network_in_range(rule.source, rule.source_mask, net_ranges)
19 | or network_in_range(rule.dest, rule.dest_mask, net_ranges)
20 | ]
21 |
22 |
23 | def split_rules_for_user(net_ranges, rules):
24 | """
25 | Return rules matching user net ranges and the rest
26 | :param net_ranges: list of network ranges
27 | :param rules: list of rules (ipv4 or ipv6
28 | :return: filtered list of rules, rest of rules
29 | """
30 | user_rules = []
31 | rest_rules = []
32 | for rule in rules:
33 | if network_in_range(rule.source, rule.source_mask, net_ranges) or network_in_range(
34 | rule.dest, rule.dest_mask, net_ranges
35 | ):
36 | user_rules.append(rule)
37 | else:
38 | rest_rules.append(rule)
39 |
40 | return user_rules, rest_rules
41 |
42 |
43 | def filter_rtbh_rules(net_ranges, rules):
44 | """
45 | Return only rules matching user net ranges
46 | :param net_ranges: list of network ranges
47 | :param rules: list of RTBH rules
48 | :return: filtered list of rules
49 | """
50 | return [
51 | rule
52 | for rule in rules
53 | if network_in_range(rule.ipv4, rule.ipv4_mask, net_ranges)
54 | or network_in_range(rule.ipv6, rule.ipv6_mask, net_ranges)
55 | ]
56 |
57 |
58 | def split_rtbh_rules_for_user(net_ranges, rules):
59 | """
60 | Return rtbh rules matching user net ranges and the rest
61 | :param net_ranges: list of network ranges
62 | :param rules: list of RTBH rules
63 | :return: filtered list of rules, rest of original list
64 | """
65 | filtered = []
66 | read_only = []
67 | for rule in rules:
68 | if network_in_range(rule.ipv4, rule.ipv4_mask, net_ranges) or network_in_range(
69 | rule.ipv6, rule.ipv6_mask, net_ranges
70 | ):
71 | filtered.append(rule)
72 | else:
73 | read_only.append(rule)
74 |
75 | return filtered, read_only
76 |
77 |
78 | def address_in_range(address, net_ranges):
79 | """
80 | check if given ip address is in user network ranges
81 | :param address: string ip_address
82 | :param net_ranges: list of network ranges
83 | :return: boolean
84 | """
85 | result = False
86 | for adr_range in net_ranges:
87 | try:
88 | result = result or ipaddress.ip_address(address) in ipaddress.ip_network(adr_range)
89 | except ValueError:
90 | return False
91 |
92 | return result
93 |
94 |
95 | def network_in_range(address, mask, net_ranges):
96 | """
97 | check if given ip address is in user network ranges
98 | :param address: string ip_address
99 | :param net_ranges: list of network ranges
100 | :return: boolean
101 | """
102 | result = False
103 | network = "{}/{}".format(address, mask)
104 | for adr_range in net_ranges:
105 | try:
106 | result = result or subnet_of(ipaddress.ip_network(network), ipaddress.ip_network(adr_range))
107 | except TypeError: # V4 can't be a subnet of V6 and vice versa
108 | pass
109 | except ValueError:
110 | return False
111 |
112 | return result
113 |
114 |
115 | def range_in_network(address, mask, net_ranges):
116 | """
117 | check if given ip address is in user network ranges
118 | :param address: string ip_address
119 | :param net_ranges: list of network ranges
120 | :return: boolean
121 | """
122 | result = False
123 | network = "{}/{}".format(address, mask)
124 | for adr_range in net_ranges:
125 | try:
126 | result = result or supernet_of(ipaddress.ip_network(network), ipaddress.ip_network(adr_range))
127 | except ValueError:
128 | return False
129 |
130 | return result
131 |
132 |
133 | def whole_world_range(net_ranges, address="0.0.0.0"):
134 | """
135 | check if user can specify network address for whole world
136 | :param address: 0.0.0.0/0 or ::/0
137 | :param net_ranges: list of network ranges
138 | :return: boolean
139 | """
140 | result = False
141 |
142 | try:
143 | for adr_range in net_ranges:
144 | result = result or ipaddress.ip_address(address) in ipaddress.ip_network(adr_range)
145 | except ValueError:
146 | return False
147 |
148 | return result
149 |
150 |
151 | def address_with_mask(address, mask):
152 | """
153 | check if given ip address and mask combination is valid
154 | :param address: string ip_address
155 | :param mask: int net prefix/mask
156 | :return: boolean
157 | """
158 | merged = "{}/{}".format(address, mask)
159 | try:
160 | ipaddress.ip_network(merged)
161 | except ValueError:
162 | return False
163 |
164 | return True
165 |
166 |
167 | class DateNotExpired(object):
168 | """
169 | Validator for date - must be in the future
170 | """
171 |
172 | def __init__(self, message=None):
173 | if not message:
174 | message = "You can not insert expired rule. Date expired:"
175 | self.message = message
176 |
177 | def __call__(self, form, field):
178 | expires = utils.webpicker_to_datetime(field.data)
179 | if expires < datetime.now():
180 | raise ValidationError(self.message + str(field.data))
181 |
182 |
183 | class PortString(object):
184 | """
185 | Validator for port string - must be translatable to ExaBgp syntax
186 | Max number of comma separated values must be <= 6 (default)
187 | """
188 |
189 | def __init__(self, message=None, max_values=constants.MAX_COMMA_VALUES):
190 | if not message:
191 | message = "Invalid syntax: "
192 | self.message = message
193 | self.max_values = max_values
194 |
195 | def __call__(self, form, field):
196 | field_data = field.data.split(";")
197 | if len(field_data) > self.max_values:
198 | raise ValidationError("{} maximum {} comma separated values".format(self.message, self.max_values))
199 | try:
200 | for port_string in field_data:
201 | flowspec.to_exabgp_string(port_string, constants.MAX_PORT)
202 | except ValueError as e:
203 | raise ValidationError(self.message + str(e.args[0]))
204 |
205 |
206 | class PacketString(object):
207 | """
208 | Validator for packet length string - must be translatable to ExaBgp syntax
209 | """
210 |
211 | def __init__(self, message=None):
212 | if not message:
213 | message = "Invalid packet size value: "
214 | self.message = message
215 |
216 | def __call__(self, form, field):
217 | try:
218 | for port_string in field.data.split(";"):
219 | flowspec.to_exabgp_string(port_string, constants.MAX_PACKET)
220 | except ValueError as e:
221 | raise ValidationError(self.message + str(e.args[0]))
222 |
223 |
224 | class NetRangeString(object):
225 | """
226 | Validator for IP adress network range
227 | each part of string must be valid ip address separated by spaces, newlines
228 | """
229 |
230 | def __init__(self, message=None):
231 | if not message:
232 | message = "Invalid network range: "
233 | self.message = message
234 |
235 | def __call__(self, form, field):
236 | try:
237 | for net_string in field.data.split():
238 | _a = ipaddress.ip_network(net_string)
239 | except ValueError as e:
240 | raise ValidationError(self.message + str(e.args[0]))
241 |
242 |
243 | class NetInRange(object):
244 | """
245 | Validator for IP address - must be in organization net range
246 | """
247 |
248 | def __init__(self, net_ranges):
249 | self.message = "Address not in organization range : {}.".format(net_ranges)
250 | self.net_ranges = net_ranges
251 |
252 | def __call__(self, form, field):
253 | result = False
254 | for address in field.data.split("/"):
255 | for adr_range in self.net_ranges:
256 | result = result or ipaddress.ip_address(address) in ipaddress.ip_network(adr_range)
257 |
258 | if not result:
259 | raise ValidationError(self.message)
260 |
261 |
262 | class IPAddress(object):
263 | """
264 | Validator for IP Address
265 | """
266 |
267 | def __init__(self, message=None):
268 | if not message:
269 | message = "This does not look like valid IP Address: "
270 | self.message = message
271 |
272 | def __call__(self, form, field):
273 | try:
274 | ipaddress.ip_address(field.data)
275 | except ValueError:
276 | raise ValidationError(self.message + str(field.data))
277 |
278 |
279 | class IPv6Address(object):
280 | """
281 | Validator for IPv6 address - the original from WTForms is not working for ipv6 correctly
282 | """
283 |
284 | def __init__(self, message=None):
285 | if not message:
286 | message = "This does not look like valid IPv6 Address: "
287 | self.message = message
288 |
289 | def __call__(self, form, field):
290 | try:
291 | ipaddress.IPv6Address(field.data)
292 | except ValueError:
293 | raise ValidationError(self.message + str(field.data))
294 |
295 |
296 | class IPv4Address(object):
297 | """
298 | Validator for IPv4 address - the original from WTForms is not working for ipv6 correctly
299 | """
300 |
301 | def __init__(self, message=None):
302 | if not message:
303 | message = "This does not look like valid IPv4 Address: "
304 | self.message = message
305 |
306 | def __call__(self, form, field):
307 | try:
308 | ipaddress.IPv4Address(field.data)
309 | except ValueError:
310 | raise ValidationError(self.message + str(field.data))
311 |
312 |
313 | def editable_range(rule, net_ranges):
314 | """
315 | check if the rule is editable for user
316 | choice is based on user network ranges
317 | :param net_ranges: list of user networks
318 | :param rule: object IPV4 or IPV6 rule
319 | """
320 | result = False
321 |
322 | for adr_range in net_ranges:
323 | net = ipaddress.ip_network(adr_range)
324 | if rule.source and ipaddress.ip_address(rule.source) in net:
325 | adr, pref = adr_range.split("/")
326 | if rule.source_mask and int(rule.source_mask) >= int(pref):
327 | result = True
328 |
329 | if rule.dest and ipaddress.ip_address(rule.dest) in net:
330 | adr, pref = adr_range.split("/")
331 | if rule.dest_mask and int(rule.dest_mask) >= int(pref):
332 | result = True
333 |
334 | return result
335 |
336 |
337 | def _is_subnet_of(a, b):
338 | try:
339 | # Always false if one is v4 and the other is v6.
340 | if a._version != b._version:
341 | raise TypeError("%s and %s are not of the same version" % (a, b))
342 | return b.network_address <= a.network_address and b.broadcast_address >= a.broadcast_address
343 | except AttributeError:
344 | raise TypeError("Unable to test subnet containment " "between %s and %s" % (a, b))
345 |
346 |
347 | def subnet_of(net_a, net_b):
348 | """Return True if this network is a subnet of other."""
349 | return _is_subnet_of(net_a, net_b)
350 |
351 |
352 | def supernet_of(net_a, net_b):
353 | """Return True if this network is a supernet of other."""
354 | return _is_subnet_of(net_b, net_a)
355 |
--------------------------------------------------------------------------------
/flowapp/views/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CESNET/exafs/866f3bab1b207fccc6118efeaa4281a243a36e1b/flowapp/views/__init__.py
--------------------------------------------------------------------------------
/flowapp/views/api_keys.py:
--------------------------------------------------------------------------------
1 | import jwt
2 | from flask import (
3 | Blueprint,
4 | render_template,
5 | redirect,
6 | flash,
7 | request,
8 | url_for,
9 | session,
10 | make_response,
11 | current_app,
12 | )
13 | import secrets
14 |
15 | from ..forms import ApiKeyForm
16 | from ..models import ApiKey
17 | from ..auth import auth_required
18 |
19 | from flowapp import db
20 |
21 | COOKIE_KEY = "keylist"
22 |
23 | api_keys = Blueprint("api_keys", __name__, template_folder="templates")
24 |
25 |
26 | @api_keys.route("/", methods=["GET"])
27 | @auth_required
28 | def all():
29 | """
30 | Show user api keys
31 | :return: page with keys
32 | """
33 | jwt_key = current_app.config.get("JWT_SECRET")
34 | keys = db.session.query(ApiKey).filter_by(user_id=session["user_id"]).all()
35 | payload = {"keys": [key.id for key in keys]}
36 | encoded = jwt.encode(payload, jwt_key, algorithm="HS256")
37 |
38 | resp = make_response(render_template("pages/api_key.html", keys=keys))
39 |
40 | if current_app.config.get("DEVEL"):
41 | resp.set_cookie(COOKIE_KEY, encoded, httponly=True, samesite="Lax")
42 | else:
43 | resp.set_cookie(COOKIE_KEY, encoded, secure=True, httponly=True, samesite="Lax")
44 |
45 | return resp
46 |
47 |
48 | @api_keys.route("/add", methods=["GET", "POST"])
49 | @auth_required
50 | def add():
51 | """
52 | Add new ApiKey
53 | :return: form or redirect to list of keys
54 | """
55 | generated = secrets.token_hex(24)
56 | form = ApiKeyForm(request.form, key=generated)
57 |
58 | if request.method == "POST" and form.validate():
59 | model = ApiKey(
60 | machine=form.machine.data,
61 | key=form.key.data,
62 | expires=form.expires.data,
63 | readonly=form.readonly.data,
64 | comment=form.comment.data,
65 | user_id=session["user_id"],
66 | org_id=session["user_org_id"],
67 | )
68 |
69 | db.session.add(model)
70 | db.session.commit()
71 | flash("NewKey saved", "alert-success")
72 |
73 | return redirect(url_for("api_keys.all"))
74 | else:
75 | for field, errors in form.errors.items():
76 | for error in errors:
77 | current_app.logger.debug("Error in the %s field - %s" % (getattr(form, field).label.text, error))
78 |
79 | return render_template("forms/api_key.html", form=form, generated_key=generated)
80 |
81 |
82 | @api_keys.route("/delete/", methods=["GET"])
83 | @auth_required
84 | def delete(key_id):
85 | """
86 | Delete api_key and machine
87 | :param key_id: integer
88 | """
89 | key_list = request.cookies.get(COOKIE_KEY)
90 | key_list = jwt.decode(key_list, current_app.config.get("JWT_SECRET"), algorithms=["HS256"])
91 |
92 | model = db.session.get(ApiKey, key_id)
93 | if model.id not in key_list["keys"]:
94 | flash("You can't delete this key!", "alert-danger")
95 | elif model.user_id == session["user_id"] or 3 in session["user_role_ids"]:
96 | # delete from db
97 | db.session.delete(model)
98 | db.session.commit()
99 | flash("Key deleted", "alert-success")
100 | else:
101 | flash("You can't delete this key!", "alert-danger")
102 |
103 | return redirect(url_for("api_keys.all"))
104 |
--------------------------------------------------------------------------------
/flowapp/views/api_v1.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify
2 |
3 |
4 | api = Blueprint("api_v1", __name__, template_folder="templates")
5 | METHODS = ["GET", "POST", "PUT", "DELETE"]
6 |
7 | @api.route("/", defaults={"path": ""}, methods=METHODS)
8 | @api.route("/", methods=METHODS)
9 | def deprecated_warning(path):
10 | """Catch all routes and return a deprecated warning message."""
11 | message = "Warning: This API version is deprecated. Please use /api/v3/ instead."
12 | return jsonify({"message": message}), 400
13 |
--------------------------------------------------------------------------------
/flowapp/views/api_v2.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify
2 |
3 |
4 | api = Blueprint("api_v2", __name__, template_folder="templates")
5 | METHODS = ["GET", "POST", "PUT", "DELETE"]
6 |
7 | @api.route("/", defaults={"path": ""}, methods=METHODS)
8 | @api.route("/", methods=METHODS)
9 | def deprecated_warning(path):
10 | """Catch all routes and return a deprecated warning message."""
11 | message = "Warning: This API version is deprecated. Please use /api/v3/ instead."
12 | return jsonify({"message": message}), 400
13 |
--------------------------------------------------------------------------------
/flowapp/views/api_v3.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request
2 | from flowapp import csrf
3 | from flowapp.views import api_common
4 |
5 | api = Blueprint("api_v3", __name__, template_folder="templates")
6 |
7 |
8 | @api.route("/auth", methods=["GET"])
9 | def authorize():
10 | mkey = request.headers.get("x-api-key", None)
11 | return api_common.authorize(mkey)
12 |
13 |
14 | @api.route("/rules")
15 | @api_common.token_required
16 | def index(current_user):
17 | key_map = {
18 | "ipv4_rules": "flowspec_ipv4_rw",
19 | "ipv6_rules": "flowspec_ipv6_rw",
20 | "rtbh_rules": "rtbh_any_rw",
21 | "ipv4_rules_readonly": "flowspec_ipv4_ro",
22 | "ipv6_rules_readonly": "flowspec_ipv6_ro",
23 | "rtbh_rules_readonly": "rtbh_any_ro",
24 | }
25 | return api_common.index(current_user, key_map)
26 |
27 |
28 | @api.route("/actions")
29 | @api_common.token_required
30 | def all_actions(current_user):
31 | """
32 | Returns Actions allowed for current user
33 | :param current_user:
34 | :return: json response
35 | """
36 | return api_common.all_actions(current_user)
37 |
38 |
39 | @api.route("/communities")
40 | @api_common.token_required
41 | def all_communities(current_user):
42 | """
43 | Returns RTHB communites allowed for current user
44 | :param current_user:
45 | :return: json response
46 | """
47 | return api_common.all_communities(current_user)
48 |
49 |
50 | @api.route("/rules/ipv4", methods=["POST"])
51 | @api_common.token_required
52 | @api_common.check_readonly
53 | def create_ipv4(current_user):
54 | """
55 | Api method for new IPv4 rule
56 | :param data: parsed json request
57 | :param current_user: data from jwt token
58 | :return: json response
59 | """
60 | return api_common.create_ipv4(current_user)
61 |
62 |
63 | @api.route("/rules/ipv6", methods=["POST"])
64 | @csrf.exempt
65 | @api_common.token_required
66 | @api_common.check_readonly
67 | def create_ipv6(current_user):
68 | """
69 | Create new IPv6 rule
70 | :param data: parsed json request
71 | :param current_user: data from jwt token
72 | :return:
73 | """
74 | return api_common.create_ipv6(current_user)
75 |
76 |
77 | @api.route("/rules/rtbh", methods=["POST"])
78 | @csrf.exempt
79 | @api_common.token_required
80 | @api_common.check_readonly
81 | def create_rtbh(current_user):
82 | return api_common.create_rtbh(current_user)
83 |
84 |
85 | @api.route("/rules/ipv4/", methods=["GET"])
86 | @api_common.token_required
87 | def ipv4_rule_get(current_user, rule_id):
88 | """
89 | Return IPv4 rule
90 | :param current_user:
91 | :param rule_id:
92 | :return:
93 | """
94 | return api_common.ipv4_rule_get(current_user, rule_id)
95 |
96 |
97 | @api.route("/rules/ipv6/", methods=["GET"])
98 | @api_common.token_required
99 | def ipv6_rule_get(current_user, rule_id):
100 | """
101 | Return IPv6 rule
102 | :param current_user:
103 | :param rule_id:
104 | :return:
105 | """
106 | return api_common.ipv6_rule_get(current_user, rule_id)
107 |
108 |
109 | @api.route("/rules/rtbh/", methods=["GET"])
110 | @api_common.token_required
111 | def rtbh_rule_get(current_user, rule_id):
112 | """
113 | Return RTBH rule
114 | :param current_user:
115 | :param rule_id:
116 | :return:
117 | """
118 | return api_common.rtbh_rule_get(current_user, rule_id)
119 |
120 |
121 | @api.route("/rules/ipv4/", methods=["DELETE"])
122 | @api_common.token_required
123 | @api_common.check_readonly
124 | def delete_v4_rule(current_user, rule_id):
125 | """
126 | Delete rule with given id and type
127 | :param rule_id: integer - rule id
128 | """
129 | return api_common.delete_v4_rule(current_user, rule_id)
130 |
131 |
132 | @api.route("/rules/ipv6/", methods=["DELETE"])
133 | @api_common.token_required
134 | @api_common.check_readonly
135 | def delete_v6_rule(current_user, rule_id):
136 | """
137 | Delete rule with given id and type
138 | :param rule_id: integer - rule id
139 | """
140 | return api_common.delete_v6_rule(current_user, rule_id)
141 |
142 |
143 | @api.route("/rules/rtbh/", methods=["DELETE"])
144 | @api_common.token_required
145 | @api_common.check_readonly
146 | def delete_rtbh_rule(current_user, rule_id):
147 | """
148 | Delete rule with given id and type
149 | :param rule_id: integer - rule id
150 | """
151 | return api_common.delete_rtbh_rule(current_user, rule_id)
152 |
153 |
154 | @api.route("/test_token", methods=["GET"])
155 | @api_common.token_required
156 | def token_test_get(current_user):
157 | """
158 | Return IPv4 rule
159 | :param current_user:
160 | :param rule_id:
161 | :return:
162 | """
163 | return api_common.token_test_get(current_user)
164 |
--------------------------------------------------------------------------------
/requirements-backup.txt:
--------------------------------------------------------------------------------
1 | appdirs>=1.4.2
2 | asn1crypto>=0.24.0
3 | attrs>=17.4.0
4 | blinker>=1.4
5 | certifi>=2018.8.24
6 | cffi>=1.11.5
7 | chardet>=3.0.4
8 | click>=6.7
9 | cryptography>=2.3
10 | cryptography-vectors>=2.3.1
11 | enum34>=1.1.6
12 | Flask>=1.0.2
13 | Flask-SQLAlchemy>=2.2
14 | Flask-SSO>=0.4.0
15 | Flask-WTF>=0.14.2
16 | funcsigs>=1.0.2
17 | honcho>=0.7.1
18 | idna>=2.7
19 | ipaddress>=1.0.22
20 | itsdangerous>=0.24
21 | Jinja2>=2.10
22 | MarkupSafe>=0.23
23 | MySQL-python>=1.2.5
24 | packaging>=16.8
25 | pluggy>=0.6.0
26 | py>=1.5.2
27 | pycparser>=2.18
28 | PyJWT>=1.6.4
29 | PyMySQL>=0.7.10
30 | pyparsing>=2.1.10
31 | pytest>=3.4.0
32 | requests>=2.20.0
33 | six>=1.11.0
34 | SQLAlchemy>=1.1.6
35 | urllib3>=1.21.1
36 | uWSGI>=2.0.14
37 | Werkzeug>=0.14.1
38 | WTForms>=2.1
39 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask>=2.0.2
2 | Flask-SQLAlchemy>=2.2
3 | Flask-SSO>=0.4.0
4 | Flask-WTF>=1.0.0
5 | Flask-Migrate>=3.0.0
6 | Flask-Script>=2.0.0
7 | Flask-Session
8 | PyJWT>=2.4.0
9 | PyMySQL>=1.0.0
10 | pytest>=7.0.0
11 | requests>=2.20.0
12 | babel>=2.7.0
13 | email_validator>=1.1
14 | pika>=1.3.0
15 | loguru
16 | flasgger
17 | python-dotenv
--------------------------------------------------------------------------------
/run.example.py:
--------------------------------------------------------------------------------
1 | """
2 | This is an example of how to run the application.
3 | First copy the file as run.py (or whatever you want)
4 | Then edit the file to match your needs.
5 |
6 | From version 0.8.1 the application is using Flask-Session
7 | stored in DB using SQL Alchemy driver. This can be configured for other
8 | drivers, however server side session is required for the application.
9 |
10 | In general you should not need to edit this example file.
11 | Only if you want to configure the application main menu and
12 | dashboard.
13 |
14 | Or in case that you want to add extensions etc.
15 | """
16 |
17 | from os import environ
18 |
19 | from flowapp import create_app, db, sess
20 | import config
21 |
22 |
23 | # Configurations
24 | env = environ.get("EXAFS_ENV", "Production")
25 |
26 | # Call app factory
27 | if env == "devel":
28 | app = create_app(config.DevelopmentConfig)
29 | else:
30 | app = create_app(config.ProductionConfig)
31 |
32 | # init database object
33 | db.init_app(app)
34 |
35 | # init session
36 | app.config.update(SESSION_TYPE="sqlalchemy")
37 | app.config.update(SESSION_SQLALCHEMY=db)
38 | sess.init_app(app)
39 |
40 |
41 | # run app
42 | if __name__ == "__main__":
43 | app.run(host="::", port=8080, debug=True)
44 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 109
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """
2 | Author(s):
3 | Jiri Vrany
4 | Petr Adamec
5 | Jakub Man
6 |
7 | Setuptools configuration
8 | """
9 |
10 | import setuptools
11 |
12 | # Import the __version__ variable without having to import the flowapp package.
13 | # This prevents missing dependency error in new virtual environments.
14 | with open("flowapp/__about__.py") as f:
15 | exec(f.read())
16 |
17 | setuptools.setup(
18 | name="exafs",
19 | version=__version__, # noqa: F821
20 | author="CESNET / Jiri Vrany, Petr Adamec, Josef Verich, Jakub Man",
21 | description="Tool for creation, validation, and execution of ExaBGP messages.",
22 | url="https://github.com/CESNET/exafs",
23 | license="MIT",
24 | py_modules=[
25 | "flowapp",
26 | ],
27 | packages=setuptools.find_packages(),
28 | include_package_data=True,
29 | python_requires=">=3.11",
30 | install_requires=[
31 | "Flask>=2.0.2",
32 | "Flask-SQLAlchemy>=2.2",
33 | "Flask-SSO>=0.4.0",
34 | "Flask-WTF>=1.0.0",
35 | "Flask-Migrate>=3.0.0",
36 | "Flask-Script>=2.0.0",
37 | "PyJWT>=2.4.0",
38 | "PyMySQL>=1.0.0",
39 | "pytest>=7.0.0",
40 | "requests>=2.20.0",
41 | "babel>=2.7.0",
42 | "mysqlclient>=2.0.0",
43 | "email_validator>=1.1",
44 | "pika>=1.3.0",
45 | ],
46 | )
47 |
--------------------------------------------------------------------------------