mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-30 10:56:28 +02:00
dashboard: improve display of device credentials, allow deletion (#2829)
* dashboard: improve display of device credentials, allow deletion * fix test
This commit is contained in:
parent
c064bc8e0e
commit
838c9e3a3d
8 changed files with 225 additions and 36 deletions
|
@ -29,7 +29,6 @@ import (
|
||||||
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
||||||
"github.com/pomerium/pomerium/internal/urlutil"
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
"github.com/pomerium/pomerium/pkg/cryptutil"
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/device"
|
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||||
|
@ -490,11 +489,26 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
|
||||||
return fmt.Errorf("invalid webauthn url: %w", err)
|
return fmt.Errorf("invalid webauthn url: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var deviceCredentials []*device.Credential
|
type DeviceCredentialInfo struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
var currentDeviceCredentials, otherDeviceCredentials []DeviceCredentialInfo
|
||||||
for _, id := range pbUser.GetDeviceCredentialIds() {
|
for _, id := range pbUser.GetDeviceCredentialIds() {
|
||||||
deviceCredentials = append(deviceCredentials, &device.Credential{
|
selected := false
|
||||||
Id: id,
|
for _, c := range pbSession.GetDeviceCredentials() {
|
||||||
|
if c.GetId() == id {
|
||||||
|
selected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if selected {
|
||||||
|
currentDeviceCredentials = append(currentDeviceCredentials, DeviceCredentialInfo{
|
||||||
|
ID: id,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
otherDeviceCredentials = append(otherDeviceCredentials, DeviceCredentialInfo{
|
||||||
|
ID: id,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input := map[string]interface{}{
|
input := map[string]interface{}{
|
||||||
|
@ -502,7 +516,8 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
|
||||||
"State": s, // local session state (cookie, header, etc)
|
"State": s, // local session state (cookie, header, etc)
|
||||||
"Session": pbSession, // current access, refresh, id token
|
"Session": pbSession, // current access, refresh, id token
|
||||||
"User": pbUser, // user details inferred from oidc id_token
|
"User": pbUser, // user details inferred from oidc id_token
|
||||||
"DeviceCredentials": deviceCredentials,
|
"CurrentDeviceCredentials": currentDeviceCredentials,
|
||||||
|
"OtherDeviceCredentials": otherDeviceCredentials,
|
||||||
"DirectoryUser": pbDirectoryUser, // user details inferred from idp directory
|
"DirectoryUser": pbDirectoryUser, // user details inferred from idp directory
|
||||||
"DirectoryGroups": groups, // user's groups inferred from idp directory
|
"DirectoryGroups": groups, // user's groups inferred from idp directory
|
||||||
"csrfField": csrf.TemplateField(r),
|
"csrfField": csrf.TemplateField(r),
|
||||||
|
|
32
authenticate/handlers/webauthn/helpers.go
Normal file
32
authenticate/handlers/webauthn/helpers.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package webauthn
|
||||||
|
|
||||||
|
import "github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
|
|
||||||
|
func containsString(elements []string, value string) bool {
|
||||||
|
for _, element := range elements {
|
||||||
|
if element == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeString(elements []string, value string) []string {
|
||||||
|
dup := make([]string, 0, len(elements))
|
||||||
|
for _, element := range elements {
|
||||||
|
if element != value {
|
||||||
|
dup = append(dup, element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dup
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeSessionDeviceCredential(elements []*session.Session_DeviceCredential, id string) []*session.Session_DeviceCredential {
|
||||||
|
dup := make([]*session.Session_DeviceCredential, 0, len(elements))
|
||||||
|
for _, element := range elements {
|
||||||
|
if element.GetId() != id {
|
||||||
|
dup = append(dup, element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dup
|
||||||
|
}
|
|
@ -37,8 +37,14 @@ import (
|
||||||
const maxAuthenticateResponses = 5
|
const maxAuthenticateResponses = 5
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errMissingDeviceType = httputil.NewError(http.StatusBadRequest, errors.New("device_type is a required parameter"))
|
errMissingDeviceCredentialID = httputil.NewError(http.StatusBadRequest, errors.New(
|
||||||
errMissingRedirectURI = httputil.NewError(http.StatusBadRequest, errors.New("pomerium_redirect_uri is a required parameter"))
|
urlutil.QueryDeviceCredentialID+" is a required parameter"))
|
||||||
|
errMissingDeviceType = httputil.NewError(http.StatusBadRequest, errors.New(
|
||||||
|
urlutil.QueryDeviceType+" is a required parameter"))
|
||||||
|
errMissingRedirectURI = httputil.NewError(http.StatusBadRequest, errors.New(
|
||||||
|
urlutil.QueryRedirectURI+" is a required parameter"))
|
||||||
|
errInvalidDeviceCredential = httputil.NewError(http.StatusBadRequest, errors.New(
|
||||||
|
"invalid device credential"))
|
||||||
)
|
)
|
||||||
|
|
||||||
// State is the state needed by the Handler to handle requests.
|
// State is the state needed by the Handler to handle requests.
|
||||||
|
@ -91,6 +97,8 @@ func (h *Handler) handle(w http.ResponseWriter, r *http.Request) error {
|
||||||
return h.handleAuthenticate(w, r, s)
|
return h.handleAuthenticate(w, r, s)
|
||||||
case r.FormValue("action") == "register":
|
case r.FormValue("action") == "register":
|
||||||
return h.handleRegister(w, r, s)
|
return h.handleRegister(w, r, s)
|
||||||
|
case r.FormValue("action") == "unregister":
|
||||||
|
return h.handleUnregister(w, r, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
return httputil.NewError(http.StatusNotFound, errors.New(http.StatusText(http.StatusNotFound)))
|
return httputil.NewError(http.StatusNotFound, errors.New(http.StatusText(http.StatusNotFound)))
|
||||||
|
@ -297,6 +305,49 @@ func (h *Handler) handleRegister(w http.ResponseWriter, r *http.Request, state *
|
||||||
return h.saveSessionAndRedirect(w, r, state, redirectURIParam)
|
return h.saveSessionAndRedirect(w, r, state, redirectURIParam)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleUnregister(w http.ResponseWriter, r *http.Request, state *State) error {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
// get the user information
|
||||||
|
u, err := user.Get(ctx, state.Client, state.Session.GetUserId())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceCredentialID := r.FormValue(urlutil.QueryDeviceCredentialID)
|
||||||
|
if deviceCredentialID == "" {
|
||||||
|
return errMissingDeviceCredentialID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure we only allow removing a device credential the user owns
|
||||||
|
if !containsString(u.GetDeviceCredentialIds(), deviceCredentialID) {
|
||||||
|
return errInvalidDeviceCredential
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the credential
|
||||||
|
deviceCredential, err := device.DeleteCredential(ctx, state.Client, deviceCredentialID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the corresponding enrollment
|
||||||
|
_, err = device.DeleteEnrollment(ctx, state.Client, deviceCredential.GetEnrollmentId())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the credential from the user
|
||||||
|
u.DeviceCredentialIds = removeString(u.DeviceCredentialIds, deviceCredentialID)
|
||||||
|
_, err = user.Put(ctx, state.Client, u)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the credential from the session
|
||||||
|
state.Session.DeviceCredentials = removeSessionDeviceCredential(state.Session.DeviceCredentials, deviceCredentialID)
|
||||||
|
return h.saveSessionAndRedirect(w, r, state, "/.pomerium")
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) handleView(w http.ResponseWriter, r *http.Request, state *State) error {
|
func (h *Handler) handleView(w http.ResponseWriter, r *http.Request, state *State) error {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
|
|
|
@ -182,10 +182,10 @@
|
||||||
<div class="messages">
|
<div class="messages">
|
||||||
<div class="box-inner">
|
<div class="box-inner">
|
||||||
<div class="category-header clearfix">
|
<div class="category-header clearfix">
|
||||||
<span class="category-title">Device Credentials</span>
|
<span class="category-title">Current Session Device Credentials</span>
|
||||||
</div>
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
{{if .DeviceCredentials}}
|
{{if .CurrentDeviceCredentials}}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -193,9 +193,17 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .DeviceCredentials}}
|
{{range .CurrentDeviceCredentials}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{.Id}}</td>
|
<td>{{.ID}}</td>
|
||||||
|
<td>
|
||||||
|
<form action="{{$.WebAuthnURL}}" method="POST">
|
||||||
|
{{$.csrfField}}
|
||||||
|
<input type="hidden" name="action" value="unregister">
|
||||||
|
<input type="hidden" name="pomerium_device_credential_id" value="{{.ID}}">
|
||||||
|
<button type="submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -204,6 +212,35 @@
|
||||||
No device credentials found!
|
No device credentials found!
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
{{if .OtherDeviceCredentials}}
|
||||||
|
<div class="box-inner">
|
||||||
|
<div class="category-header clearfix">
|
||||||
|
<span class="category-title">Other Device Credentials</span>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .OtherDeviceCredentials}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.ID}}</td>
|
||||||
|
<td>
|
||||||
|
<form action="{{$.WebAuthnURL}}" method="POST">
|
||||||
|
{{$.csrfField}}
|
||||||
|
<input type="hidden" name="action" value="unregister">
|
||||||
|
<input type="hidden" name="pomerium_device_credential_id" value="{{.ID}}">
|
||||||
|
<button type="submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
<div class="category-link">
|
<div class="category-link">
|
||||||
Register device with <a href="{{.WebAuthnURL}}">WebAuthn</a>.
|
Register device with <a href="{{.WebAuthnURL}}">WebAuthn</a>.
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -147,6 +147,9 @@ body {
|
||||||
.box-inner {
|
.box-inner {
|
||||||
padding: 35px;
|
padding: 35px;
|
||||||
}
|
}
|
||||||
|
.box-inner ~ .box-inner {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.white {
|
.white {
|
||||||
background: white;
|
background: white;
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -5,6 +5,7 @@ package urlutil
|
||||||
// conjunction with a HMAC to ensure authenticity.
|
// conjunction with a HMAC to ensure authenticity.
|
||||||
const (
|
const (
|
||||||
QueryCallbackURI = "pomerium_callback_uri"
|
QueryCallbackURI = "pomerium_callback_uri"
|
||||||
|
QueryDeviceCredentialID = "pomerium_device_credential_id"
|
||||||
QueryDeviceType = "pomerium_device_type"
|
QueryDeviceType = "pomerium_device_type"
|
||||||
QueryEnrollmentToken = "pomerium_enrollment_token" //nolint
|
QueryEnrollmentToken = "pomerium_enrollment_token" //nolint
|
||||||
QueryIsProgrammatic = "pomerium_programmatic"
|
QueryIsProgrammatic = "pomerium_programmatic"
|
||||||
|
|
|
@ -5,11 +5,65 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/pkg/encoding/base58"
|
"github.com/pomerium/pomerium/pkg/encoding/base58"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
||||||
"github.com/pomerium/pomerium/pkg/protoutil"
|
"github.com/pomerium/pomerium/pkg/protoutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DeleteCredential deletes a credential from the databroker.
|
||||||
|
func DeleteCredential(
|
||||||
|
ctx context.Context,
|
||||||
|
client databroker.DataBrokerServiceClient,
|
||||||
|
credentialID string,
|
||||||
|
) (*Credential, error) {
|
||||||
|
credential, err := GetCredential(ctx, client, credentialID)
|
||||||
|
if status.Code(err) == codes.NotFound {
|
||||||
|
return nil, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
any := protoutil.NewAny(credential)
|
||||||
|
_, err = client.Put(ctx, &databroker.PutRequest{
|
||||||
|
Record: &databroker.Record{
|
||||||
|
Type: any.GetTypeUrl(),
|
||||||
|
Id: credentialID,
|
||||||
|
Data: any,
|
||||||
|
DeletedAt: timestamppb.Now(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return credential, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteEnrollment deletes an enrollment from the databroker.
|
||||||
|
func DeleteEnrollment(
|
||||||
|
ctx context.Context,
|
||||||
|
client databroker.DataBrokerServiceClient,
|
||||||
|
enrollmentID string,
|
||||||
|
) (*Enrollment, error) {
|
||||||
|
enrollment, err := GetEnrollment(ctx, client, enrollmentID)
|
||||||
|
if status.Code(err) == codes.NotFound {
|
||||||
|
return nil, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
any := protoutil.NewAny(enrollment)
|
||||||
|
_, err = client.Put(ctx, &databroker.PutRequest{
|
||||||
|
Record: &databroker.Record{
|
||||||
|
Type: any.GetTypeUrl(),
|
||||||
|
Id: enrollmentID,
|
||||||
|
Data: any,
|
||||||
|
DeletedAt: timestamppb.Now(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return enrollment, err
|
||||||
|
}
|
||||||
|
|
||||||
// GetCredential gets a credential from the databroker.
|
// GetCredential gets a credential from the databroker.
|
||||||
func GetCredential(
|
func GetCredential(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|
Loading…
Add table
Reference in a new issue