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/urlutil"
|
||||
"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/session"
|
||||
"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)
|
||||
}
|
||||
|
||||
var deviceCredentials []*device.Credential
|
||||
type DeviceCredentialInfo struct {
|
||||
ID string
|
||||
}
|
||||
var currentDeviceCredentials, otherDeviceCredentials []DeviceCredentialInfo
|
||||
for _, id := range pbUser.GetDeviceCredentialIds() {
|
||||
deviceCredentials = append(deviceCredentials, &device.Credential{
|
||||
Id: id,
|
||||
selected := false
|
||||
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{}{
|
||||
|
@ -502,7 +516,8 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
|
|||
"State": s, // local session state (cookie, header, etc)
|
||||
"Session": pbSession, // current access, refresh, 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
|
||||
"DirectoryGroups": groups, // user's groups inferred from idp directory
|
||||
"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
|
||||
|
||||
var (
|
||||
errMissingDeviceType = httputil.NewError(http.StatusBadRequest, errors.New("device_type is a required parameter"))
|
||||
errMissingRedirectURI = httputil.NewError(http.StatusBadRequest, errors.New("pomerium_redirect_uri is a required parameter"))
|
||||
errMissingDeviceCredentialID = httputil.NewError(http.StatusBadRequest, errors.New(
|
||||
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.
|
||||
|
@ -91,6 +97,8 @@ func (h *Handler) handle(w http.ResponseWriter, r *http.Request) error {
|
|||
return h.handleAuthenticate(w, r, s)
|
||||
case r.FormValue("action") == "register":
|
||||
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)))
|
||||
|
@ -297,6 +305,49 @@ func (h *Handler) handleRegister(w http.ResponseWriter, r *http.Request, state *
|
|||
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 {
|
||||
ctx := r.Context()
|
||||
|
||||
|
|
|
@ -182,10 +182,10 @@
|
|||
<div class="messages">
|
||||
<div class="box-inner">
|
||||
<div class="category-header clearfix">
|
||||
<span class="category-title">Device Credentials</span>
|
||||
<span class="category-title">Current Session Device Credentials</span>
|
||||
</div>
|
||||
</ul>
|
||||
{{if .DeviceCredentials}}
|
||||
{{if .CurrentDeviceCredentials}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -193,9 +193,17 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .DeviceCredentials}}
|
||||
{{range .CurrentDeviceCredentials}}
|
||||
<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>
|
||||
{{end}}
|
||||
</tbody>
|
||||
|
@ -204,6 +212,35 @@
|
|||
No device credentials found!
|
||||
{{end}}
|
||||
</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">
|
||||
Register device with <a href="{{.WebAuthnURL}}">WebAuthn</a>.
|
||||
</div>
|
||||
|
|
|
@ -147,6 +147,9 @@ body {
|
|||
.box-inner {
|
||||
padding: 35px;
|
||||
}
|
||||
.box-inner ~ .box-inner {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.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.
|
||||
const (
|
||||
QueryCallbackURI = "pomerium_callback_uri"
|
||||
QueryDeviceCredentialID = "pomerium_device_credential_id"
|
||||
QueryDeviceType = "pomerium_device_type"
|
||||
QueryEnrollmentToken = "pomerium_enrollment_token" //nolint
|
||||
QueryIsProgrammatic = "pomerium_programmatic"
|
||||
|
|
|
@ -5,11 +5,65 @@ import (
|
|||
"context"
|
||||
"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/grpc/databroker"
|
||||
"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.
|
||||
func GetCredential(
|
||||
ctx context.Context,
|
||||
|
|
Loading…
Add table
Reference in a new issue