policies: add GeoIP policy (#10454)

* add GeoIP policy

* handle empty lists of ASNs and countries

* handle missing GeoIP database or missing IP from the database

The exceptions raised here are `PolicyException`s to let admins bypass
an execution failure.

* fix translations

whoops

* remove `GeoIPPolicyMode`

Use the policy binding's `negate` option instead

* fix `DataProvision` typing

`ak-dual-select-provider` can handle unpaginated data

* use `django-countries` instead of a static list of countries for ISO-3166

* simplify `GeoIPPolicyForm`

* pass `GeoIPPolicy` on empty policy

* add backend tests to `GeoIPPolicy`

* revise translations

* move `iso-3166/` to `policies/geoip_iso3166/`

* add client-side caching to ISO3166 API call

* fix `GeoIPPolicy` creation

The automatically generated APIs can't seem to handle `CountryField`,
so I'll have to do this by hand too.

* add docs for GeoIP Policy

* docs: stylize

add review suggestions from @tanberry

* refactor `GeoIPPolicy` API

It is now as declarative as I could make it.

* clean up `api.py` and `views.py`
This commit is contained in:
Simonyi Gergő
2024-08-06 12:37:29 +02:00
committed by GitHub
parent 87858afaf3
commit f7b16ed723
22 changed files with 1650 additions and 10 deletions

View File

@ -0,0 +1,125 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/ak-dual-select";
import { DataProvision, DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { DetailedCountry, GeoIPPolicy, PoliciesApi } from "@goauthentik/api";
import { countryCache } from "./CountryCache";
function countryToPair(country: DetailedCountry): DualSelectPair {
return [country.code, country.name];
}
@customElement("ak-policy-geoip-form")
export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
loadInstance(pk: string): Promise<GeoIPPolicy> {
return new PoliciesApi(DEFAULT_CONFIG).policiesGeoipRetrieve({
policyUuid: pk,
});
}
async send(data: GeoIPPolicy): Promise<GeoIPPolicy> {
if (data.asns?.toString() === "") {
data.asns = [];
} else {
data.asns = (data.asns as unknown as string).split(",").map(Number);
}
if (this.instance) {
return new PoliciesApi(DEFAULT_CONFIG).policiesGeoipUpdate({
policyUuid: this.instance.pk || "",
geoIPPolicyRequest: data,
});
} else {
return new PoliciesApi(DEFAULT_CONFIG).policiesGeoipCreate({
geoIPPolicyRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <span>
${msg(
"Ensure the user satisfies requirements of geography or network topology, based on IP address. If any of the configured values match, the policy passes.",
)}
</span>
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${this.instance?.name ?? ""}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="executionLogging">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${this.instance?.executionLogging ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Execution logging")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Policy-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("ASNs")} name="asns">
<input
type="text"
value="${this.instance?.asns ?? ""}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${msg(
"List of autonomous system numbers. Comma separated. E.g. 13335, 15169, 20940",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Countries")} name="countries">
<ak-dual-select-provider
.provider=${(page: number, search?: string): Promise<DataProvision> => {
return countryCache
.getCountries()
.then((results) => {
if (!search) return results;
return results.filter((result) =>
result.name
.toLowerCase()
.includes(search.toLowerCase()),
);
})
.then((results) => {
return {
options: results.map(countryToPair),
};
});
}}
.selected=${(this.instance?.countriesObj ?? []).map(countryToPair)}
available-label="${msg("Available Countries")}"
selected-label="${msg("Selected Countries")}"
>
</ak-dual-select-provider>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
}
}