dashboard: improve display of device credentials, allow deletion (#2829)

* dashboard: improve display of device credentials, allow deletion

* fix test
This commit is contained in:
Caleb Doxsey 2021-12-20 12:19:54 -07:00 committed by GitHub
parent c064bc8e0e
commit 838c9e3a3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 225 additions and 36 deletions

View file

@ -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),

View 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
}

View file

@ -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()

View file

@ -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>

View file

@ -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

View file

@ -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"

View file

@ -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,