Skip to content

Commit

Permalink
split and add vpn configuration in UI
Browse files Browse the repository at this point in the history
  • Loading branch information
wardviaene committed Aug 20, 2024
1 parent bf40283 commit 4eecb39
Show file tree
Hide file tree
Showing 6 changed files with 503 additions and 234 deletions.
3 changes: 2 additions & 1 deletion pkg/rest/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ func (c *Context) getRouter(assets fs.FS, indexHtml []byte) *http.ServeMux {
mux.Handle("/api/oidc", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.oidcProviderHandler)))))
mux.Handle("/api/oidc-renew-tokens", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.oidcRenewTokensHandler)))))
mux.Handle("/api/oidc/{id}", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.oidcProviderElementHandler)))))
mux.Handle("/api/setup", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.setupHandler)))))
mux.Handle("/api/setup/general", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.setupHandler)))))
mux.Handle("/api/setup/vpn", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.vpnSetupHandler)))))
mux.Handle("/api/scim-setup", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.scimSetupHandler)))))
mux.Handle("/api/saml-setup", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.samlSetupHandler)))))
mux.Handle("/api/saml-setup/{id}", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.samlSetupElementHandler)))))
Expand Down
55 changes: 46 additions & 9 deletions pkg/rest/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,14 @@ func (c *Context) contextHandler(w http.ResponseWriter, r *http.Request) {
}

func (c *Context) setupHandler(w http.ResponseWriter, r *http.Request) {
vpnConfig, err := wireguard.GetVPNConfig(c.Storage.Client)
if err != nil {
c.returnError(w, fmt.Errorf("could not get vpn config: %s", err), http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodGet:
setupRequest := SetupRequest{
setupRequest := GeneralSetupRequest{
Hostname: c.Hostname,
EnableTLS: c.EnableTLS,
RedirectToHttps: c.RedirectToHttps,
DisableLocalAuth: c.LocalAuthDisabled,
EnableOIDCTokenRenewal: c.EnableOIDCTokenRenewal,
Routes: strings.Join(vpnConfig.ClientRoutes, ", "),
VPNEndpoint: vpnConfig.Endpoint,
}
out, err := json.Marshal(setupRequest)
if err != nil {
Expand All @@ -123,7 +116,7 @@ func (c *Context) setupHandler(w http.ResponseWriter, r *http.Request) {
}
c.write(w, out)
case http.MethodPost:
var setupRequest SetupRequest
var setupRequest GeneralSetupRequest
decoder := json.NewDecoder(r.Body)
decoder.Decode(&setupRequest)
if c.Hostname != setupRequest.Hostname {
Expand All @@ -145,6 +138,50 @@ func (c *Context) setupHandler(w http.ResponseWriter, r *http.Request) {
c.EnableOIDCTokenRenewal = setupRequest.EnableOIDCTokenRenewal
c.OIDCRenewal.SetEnabled(c.EnableOIDCTokenRenewal)
}
err := SaveConfig(c)
if err != nil {
c.returnError(w, fmt.Errorf("could not save config to disk: %s", err), http.StatusBadRequest)
return
}
out, err := json.Marshal(setupRequest)
if err != nil {
c.returnError(w, fmt.Errorf("could not marshal SetupRequest: %s", err), http.StatusBadRequest)
return
}
c.write(w, out)
default:
c.returnError(w, fmt.Errorf("method not supported"), http.StatusBadRequest)
}
}

func (c *Context) vpnSetupHandler(w http.ResponseWriter, r *http.Request) {
vpnConfig, err := wireguard.GetVPNConfig(c.Storage.Client)
if err != nil {
c.returnError(w, fmt.Errorf("could not get vpn config: %s", err), http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodGet:
setupRequest := VPNSetupRequest{
Routes: strings.Join(vpnConfig.ClientRoutes, ", "),
VPNEndpoint: vpnConfig.Endpoint,
AddressRange: vpnConfig.AddressRange.String(),
ClientAddressPrefix: vpnConfig.ClientAddressPrefix,
Port: vpnConfig.Port,
ExternalInterface: vpnConfig.ExternalInterface,
Nameservers: strings.Join(vpnConfig.Nameservers, ","),
DisableNAT: vpnConfig.DisableNAT,
}
out, err := json.Marshal(setupRequest)
if err != nil {
c.returnError(w, fmt.Errorf("could not marshal SetupRequest: %s", err), http.StatusBadRequest)
return
}
c.write(w, out)
case http.MethodPost:
var setupRequest VPNSetupRequest
decoder := json.NewDecoder(r.Body)
decoder.Decode(&setupRequest)
if strings.Join(vpnConfig.ClientRoutes, ", ") != setupRequest.Routes {
networks := strings.Split(setupRequest.Routes, ",")
validatedNetworks := []string{}
Expand Down
13 changes: 12 additions & 1 deletion pkg/rest/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ type UserInfoResponse struct {
UserType string `json:"userType"`
}

type SetupRequest struct {
type GeneralSetupRequest struct {
Hostname string `json:"hostname"`
EnableTLS bool `json:"enableTLS"`
RedirectToHttps bool `json:"redirectToHttps"`
Expand All @@ -103,6 +103,17 @@ type SetupRequest struct {
VPNEndpoint string `json:"vpnEndpoint"`
}

type VPNSetupRequest struct {
Routes string `json:"routes"`
VPNEndpoint string `json:"vpnEndpoint"`
AddressRange string `json:"addressRange"`
ClientAddressPrefix string `json:"clientAddressPrefix"`
Port int `json:"port"`
ExternalInterface string `json:"externalInterface"`
Nameservers string `json:"nameservers"`
DisableNAT bool `json:"disableNAT"`
}

type NewConnectionResponse struct {
Name string `json:"name"`
}
Expand Down
195 changes: 195 additions & 0 deletions webapp/src/Routes/Setup/GeneralSetup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { Text, Checkbox, Container, UnstyledButton, Tooltip, Center, rem, TextInput, Space, Button, Alert } from "@mantine/core";
import classes from './Setup.module.css';
import { useEffect, useState } from "react";
import { IconInfoCircle } from "@tabler/icons-react";
import { AppSettings } from "../../Constants/Constants";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useAuthContext } from "../../Auth/Auth";
import { useForm } from '@mantine/form';
import axios, { AxiosError } from "axios";

type GeneralSetupRequest = {
hostname: string;
enableTLS: boolean;
redirectToHttps: boolean;
disableLocalAuth: boolean;
enableOIDCTokenRenewal: boolean;
};
export function GeneralSetup() {
const [saved, setSaved] = useState(false)
const [saveError, setSaveError] = useState("")
const {authInfo} = useAuthContext();
const queryClient = useQueryClient()
const { isPending, error, data, isSuccess } = useQuery({
queryKey: ['general-setup'],
queryFn: () =>
fetch(AppSettings.url + '/setup/general', {
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + authInfo.token
},
}).then((res) => {
return res.json()
}

),
})
const form = useForm({
mode: 'uncontrolled',
initialValues: {
hostname: "",
enableTLS: false,
redirectToHttps: false,
disableLocalAuth: false,
enableOIDCTokenRenewal: false,
},
});
const alertIcon = <IconInfoCircle />;
const setupMutation = useMutation({
mutationFn: (setupRequest: GeneralSetupRequest) => {
return axios.post(AppSettings.url + '/setup', setupRequest, {
headers: {
"Authorization": "Bearer " + authInfo.token
},
})
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
setSaved(true)
},
onError: (error:AxiosError) => {
setSaveError("Error: "+ error.message)
}
})


useEffect(() => {
if (isSuccess) {
form.setValues({ ...data });
}
}, [isSuccess]);


const hostnameTooltip = (
<Tooltip
label="The server hostname. This hostname will be use to request Let's encrypt TLS certificates when TLS is enabled"
position="top-end"
withArrow
transitionProps={{ transition: 'pop-bottom-right' }}
>
<Text component="div" c="dimmed" style={{ cursor: 'help' }}>
<Center>
<IconInfoCircle style={{ width: rem(18), height: rem(18) }} stroke={1.5} />
</Center>
</Text>
</Tooltip>
);

if(isPending) return "Loading..."
if(error) return 'A backend error has occurred: ' + error.message

return (
<Container my={40} size="40rem">
{saved ? <Alert variant="light" color="green" title="Update!" icon={alertIcon}>Settings Saved!</Alert> : null}
{saveError !== "" ? saveError : null}
<form onSubmit={form.onSubmit((values: GeneralSetupRequest) => setupMutation.mutate(values))}>
<TextInput
rightSection={hostnameTooltip}
label="VPN Server Hostname"
placeholder="Hostname"
key={form.key('hostname')}
{...form.getInputProps('hostname')}
/>
<Space h="md" />
<UnstyledButton className={classes.button} onClick={() => form.setFieldValue("enableTLS", !form.getValues().enableTLS )}>
<Checkbox
tabIndex={-1}
size="md"
mr="xl"
styles={{ input: { cursor: 'pointer' } }}
aria-hidden
key={form.key('enableTLS')}
{...form.getInputProps('enableTLS', { type: 'checkbox' })}
/>
<div>
<Text fw={500} mb={7} lh={1}>
Enable TLS (https)
</Text>
<Text fz="sm" c="dimmed">
Enable TLS (https) using Let's Encrypt (recommended)
</Text>
</div>
</UnstyledButton>
<Space h="md" />
<UnstyledButton className={classes.button} onClick={() => window.location.protocol === "https:" ? form.setFieldValue("redirectToHttps", !form.getValues().redirectToHttps) : null }>
<Checkbox
tabIndex={-1}
size="md"
mr="xl"
styles={{ input: { cursor: 'pointer' } }}
aria-hidden
disabled={ window.location.protocol === "https:" ? false : true }
key={form.key('redirectToHttps')}
{...form.getInputProps('redirectToHttps', { type: 'checkbox' })}
/>
<div>
<Text fw={500} mb={7} lh={1}>
Redirect http to https
</Text>
<Text fz="sm" c="dimmed">
Redirect http requests to https.
Not needed when terminating TLS on an external LoadBalancer.
Can only be enabled once this page is requested through https.
</Text>
</div>
</UnstyledButton>
<Space h="md" />
<UnstyledButton className={classes.button} onClick={() => form.setFieldValue("disableLocalAuth", !form.getValues().disableLocalAuth )}>
<Checkbox
tabIndex={-1}
size="md"
mr="xl"
styles={{ input: { cursor: 'pointer' } }}
aria-hidden
key={form.key('disableLocalAuth')}
{...form.getInputProps('disableLocalAuth', { type: 'checkbox' })}
/>
<div>
<Text fw={500} mb={7} lh={1}>
Disable local auth
</Text>
<Text fz="sm" c="dimmed">
Once an OIDC Connection is setup, you can disable local authentication. Make sure to have assigned a new admin role.
</Text>
</div>
</UnstyledButton>
<Space h="md" />
<UnstyledButton className={classes.button} onClick={() => form.setFieldValue("enableOIDCTokenRenewal", !form.getValues().enableOIDCTokenRenewal )}>
<Checkbox
tabIndex={-1}
size="md"
mr="xl"
styles={{ input: { cursor: 'pointer' } }}
aria-hidden
key={form.key('enableOIDCTokenRenewal')}
{...form.getInputProps('enableOIDCTokenRenewal', { type: 'checkbox' })}
/>
<div>
<Text fw={500} mb={7} lh={1}>
Deactivate a user's VPN connection on OIDC token renewal failure
</Text>
<Text fz="sm" c="dimmed">
OIDC Tokens can be refreshed when expired.
The OIDC tokens will be renewed, and on renewal failure, the VPN connection of that user will be disabled until the user logs in again.
</Text>
<Text fz="sm" c="dimmed" style={{marginTop: 5}}>Note: Only use this when SCIM provisioning is not possible in your setup. </Text>
</div>
</UnstyledButton>
<Button type="submit" mt="md">
Submit
</Button>
</form>
</Container>

)
}
Loading

0 comments on commit 4eecb39

Please sign in to comment.