Add SSH Key Management

This commit is contained in:
Tom Wiesing 2023-01-15 13:41:56 +01:00
parent ef76844922
commit bcd1805001
No known key found for this signature in database
62 changed files with 1004 additions and 188 deletions

View file

@ -134,14 +134,6 @@
<code>{{.Config.ConfigPath}}</code>
</td>
</tr>
<tr>
<td>
<code>authorized_keys</code>
</td>
<td>
<code>{{.Config.GlobalAuthorizedKeysFile}}</code>
</td>
</tr>
</tbody>
</table>
</div>

View file

@ -37,6 +37,9 @@ func (admin *Admin) users(r *http.Request) (uc userContext, err error) {
{Title: "Admin", Path: "/admin/"},
{Title: "Users", Path: "/admin/users/"},
},
Actions: []component.MenuItem{
{Title: "Create New", Path: "/admin/users/create/"},
},
})
uc.Error = r.URL.Query().Get("error")
@ -70,9 +73,6 @@ func (admin *Admin) createUser(ctx context.Context) http.Handler {
{Title: "Users", Path: "/admin/users"},
{Title: "Create", Path: "/admin/users/create"},
},
Actions: []component.MenuItem{
{Title: "Create New", Path: "/admin/users/create/"},
},
}
return &httpx.Form[createUserResult]{

View file

@ -2,7 +2,6 @@ HOST_RULE=${HOST_RULE}
CONFIG_PATH=${CONFIG_PATH}
DEPLOY_ROOT=${DEPLOY_ROOT}
GLOBAL_AUTHORIZED_KEYS_FILE=${GLOBAL_AUTHORIZED_KEYS_FILE}
SELF_OVERRIDES_FILE=${SELF_OVERRIDES_FILE}
SELF_RESOLVER_BLOCK_FILE=${SELF_RESOLVER_BLOCK_FILE}

View file

@ -49,9 +49,8 @@ func (control *Control) Stack(env environment.Environment) component.StackWithRe
"CONFIG_PATH": control.Config.ConfigPath,
"DEPLOY_ROOT": control.Config.DeployRoot,
"GLOBAL_AUTHORIZED_KEYS_FILE": control.Config.GlobalAuthorizedKeysFile,
"SELF_OVERRIDES_FILE": control.Config.SelfOverridesFile,
"SELF_RESOLVER_BLOCK_FILE": control.Config.SelfResolverBlockFile,
"SELF_OVERRIDES_FILE": control.Config.SelfOverridesFile,
"SELF_RESOLVER_BLOCK_FILE": control.Config.SelfResolverBlockFile,
"CUSTOM_ASSETS_PATH": control.Dependencies.Custom.CustomAssetsPath(),
},

View file

@ -2,4 +2,4 @@ FROM docker.io/library/docker:20.10-cli
COPY wdcli /wdcli
EXPOSE 8888
CMD ["/wdcli","--internal-in-docker","server","--bind","0.0.0.0:8888"]
CMD ["/wdcli","--internal-in-docker","server","--bind","0.0.0.0:8888", "--internal-bind", "0.0.0.0:9999"]

View file

@ -25,7 +25,6 @@ services:
- "/var/run/docker.sock:/var/run/docker.sock"
- "${CONFIG_PATH}:${CONFIG_PATH}:ro"
- "${DEPLOY_ROOT}:${DEPLOY_ROOT}:rw"
- "${GLOBAL_AUTHORIZED_KEYS_FILE}:${GLOBAL_AUTHORIZED_KEYS_FILE}:ro"
- "${SELF_OVERRIDES_FILE}:${SELF_OVERRIDES_FILE}:ro"
- "${SELF_RESOLVER_BLOCK_FILE}:${SELF_RESOLVER_BLOCK_FILE}:ro"
- "${CUSTOM_ASSETS_PATH}:${CUSTOM_ASSETS_PATH}:ro"

View file

@ -0,0 +1,9 @@
---
title: Reworked SSH key support
date: 2023-01-15
---
- reworked and added ssh key management to the server
- users can now add and remove ssh keys to their account
- each user with an admin grant for a specific instance has ssh access via their keys
- distillery administrators have implicit access to all instances

View file

@ -18,17 +18,17 @@ import (
// The server may spawn background tasks, but these should be terminated once context closes.
//
// Logging messages are directed to progress
func (control *Control) Server(ctx context.Context, progress io.Writer) (http.Handler, error) {
func (control *Control) Server(ctx context.Context, progress io.Writer) (public http.Handler, internal http.Handler, err error) {
logger := zerolog.Ctx(ctx)
var mux mux.Mux[component.RouteContext]
mux.Context = func(r *http.Request) component.RouteContext {
var publicM, internalM mux.Mux[component.RouteContext]
publicM.Context = func(r *http.Request) component.RouteContext {
slug, ok := control.Still.Config.SlugFromHost(r.Host)
return component.RouteContext{
DefaultDomain: slug == "" && ok,
}
}
mux.Panic = func(panic any, w http.ResponseWriter, r *http.Request) {
publicM.Panic = func(panic any, w http.ResponseWriter, r *http.Request) {
// log the panic
logger.Error().
Str("panic", fmt.Sprint(panic)).
@ -39,6 +39,10 @@ func (control *Control) Server(ctx context.Context, progress io.Writer) (http.Ha
httpx.TextInterceptor.Fallback.ServeHTTP(w, r)
}
// setup the internal server identically
internalM.Panic = publicM.Panic
internalM.Context = publicM.Context
// create a csrf protector
csrfProtector := control.CSRF()
@ -52,6 +56,7 @@ func (control *Control) Server(ctx context.Context, progress io.Writer) (http.Ha
Bool("Exact", routes.Exact).
Bool("CSRF", routes.CSRF).
Bool("Decorator", routes.Decorator != nil).
Bool("Internal", routes.Internal).
Bool("MatchAllDomains", routes.MatchAllDomains).
Msg("mounting route")
@ -69,19 +74,23 @@ func (control *Control) Server(ctx context.Context, progress io.Writer) (http.Ha
handler = routes.Decorate(handler, csrfProtector)
// determine the predicate
predicate := routes.Predicate(mux.ContextOf)
predicate := routes.Predicate(publicM.ContextOf)
// and add all the prefixes
for _, prefix := range append([]string{routes.Prefix}, routes.Aliases...) {
mux.Add(prefix, predicate, routes.Exact, handler)
if routes.Internal {
internalM.Add(prefix, predicate, routes.Exact, handler)
} else {
publicM.Add(prefix, predicate, routes.Exact, handler)
}
}
}
// apply the given context function
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(cancel.ValuesOf(r.Context(), ctx))
mux.ServeHTTP(w, r)
}), nil
public = httpx.WithContextWrapper(&publicM, func(rcontext context.Context) context.Context { return cancel.ValuesOf(rcontext, ctx) })
internal = httpx.WithContextWrapper(&internalM, func(rcontext context.Context) context.Context { return cancel.ValuesOf(rcontext, ctx) })
err = nil
return
}
// CSRF returns a CSRF handler for the given function

View file

@ -15,12 +15,12 @@ var AssetsDefault = Assets{
// AssetsUser contains assets for the 'User' entrypoint.
var AssetsUser = Assets{
Scripts: `<script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/User.4197014b.js"></script><script src="/static/User.30d54198.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Default.db26a303.css"><link rel="stylesheet" href="/static/User.38d394c2.css">`,
Scripts: `<script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/User.e0367d79.js"></script><script src="/static/User.b2f9a57c.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Default.db26a303.css"><link rel="stylesheet" href="/static/User.68febbf8.css"><link rel="stylesheet" href="/static/User.840de3b4.css">`,
}
// AssetsAdmin contains assets for the 'Admin' entrypoint.
var AssetsAdmin = Assets{
Scripts: `<script nomodule="" defer src="/static/User.30d54198.js"></script><script type="module" src="/static/User.4197014b.js"></script><script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Admin.4ca3cb6f.js"></script><script src="/static/Admin.9750ba9c.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Default.db26a303.css"><link rel="stylesheet" href="/static/Admin.6d59e220.css"><link rel="stylesheet" href="/static/User.38d394c2.css"><link rel="stylesheet" href="/static/Admin.6d2ae968.css">`,
Scripts: `<script nomodule="" defer src="/static/User.b2f9a57c.js"></script><script type="module" src="/static/User.e0367d79.js"></script><script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Admin.4ca3cb6f.js"></script><script src="/static/Admin.9750ba9c.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Default.db26a303.css"><link rel="stylesheet" href="/static/Admin.6d59e220.css"><link rel="stylesheet" href="/static/User.840de3b4.css"><link rel="stylesheet" href="/static/User.68febbf8.css"><link rel="stylesheet" href="/static/Admin.6d2ae968.css">`,
}

View file

@ -1 +0,0 @@
!function(){var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},r={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in r){var o=r[e];delete r[e];var i={id:e,exports:{}};return n[e]=i,o.call(i.exports,i,i.exports),i.exports}var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}).register=function(e,n){r[e]=n},e.parcelRequireafa4=o),o.register("kEAtK",(function(e,n){})),o("kEAtK")}();

View file

@ -1 +0,0 @@
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},r={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in r){var o=r[e];delete r[e];var i={id:e,exports:{}};return n[e]=i,o.call(i.exports,i,i.exports),i.exports}var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}).register=function(e,n){r[e]=n},e.parcelRequireafa4=o),o.register("gkpdw",(function(e,n){})),o("gkpdw");

View file

@ -0,0 +1 @@
.copy{-webkit-user-select:all;user-select:all}

View file

@ -0,0 +1 @@
textarea#key{width:50%;height:10em;resize:both}

View file

@ -0,0 +1 @@
!function(){var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},r={},n={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in r)return r[e].exports;if(e in n){var o=n[e];delete n[e];var t={id:e,exports:{}};return r[e]=t,o.call(t.exports,t,t.exports),t.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,r){n[e]=r},e.parcelRequireafa4=o),o.register("kEAtK",(function(e,r){o("15EWx")})),o.register("15EWx",(function(e,r){document.querySelectorAll(".copy").forEach((e=>{e.addEventListener("click",(()=>{navigator.clipboard&&navigator.clipboard.writeText(e.innerText)}))}))})),o("kEAtK")}();

View file

@ -0,0 +1 @@
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},r={},n={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in r)return r[e].exports;if(e in n){var o=n[e];delete n[e];var i={id:e,exports:{}};return r[e]=i,o.call(i.exports,i,i.exports),i.exports}var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}).register=function(e,r){n[e]=r},e.parcelRequireafa4=o),o.register("gkpdw",(function(e,r){o("hZNgY")})),o.register("hZNgY",(function(e,r){document.querySelectorAll(".copy").forEach((e=>{e.addEventListener("click",(()=>{navigator.clipboard&&navigator.clipboard.writeText(e.innerText)}))}))})),o("gkpdw");

View file

@ -1 +1,6 @@
/* nothing for now */
/* textarea on the /user/ssh/add form */
textarea#key {
width: 50%;
height: 10em;
resize: both;
}

View file

@ -1 +1 @@
// nothing for now
import "~/src/lib/copy"

View file

@ -0,0 +1,3 @@
.copy {
user-select: all;
}

View file

@ -0,0 +1,8 @@
import "./index.css"
document.querySelectorAll('.copy').forEach((elem: Element) => {
elem.addEventListener('click', () => {
if (!navigator.clipboard) return;
navigator.clipboard.writeText((elem as HTMLElement).innerText);
})
})