-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
885 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
package controllers | ||
|
||
import ( | ||
"encoding/hex" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/brave-experiments/accounts/datastore" | ||
"github.com/brave-experiments/accounts/middleware" | ||
"github.com/brave-experiments/accounts/util" | ||
"github.com/go-chi/chi/v5" | ||
"github.com/go-chi/render" | ||
"github.com/go-playground/validator/v10" | ||
"github.com/google/uuid" | ||
) | ||
|
||
const keyNameURLParam = "name" | ||
|
||
// UserKey represents the HTTP request format for a key | ||
type UserKeyStoreRequest struct { | ||
// Name identifies the type of key (wrapping_key or sync_enc_seed) | ||
Name string `json:"name" validate:"required,oneof=wrapping_key sync_enc_seed"` | ||
// EncryptedKey contains the encrypted key data as hex bytes | ||
EncryptedKey string `json:"encryptedKey" validate:"required,min=16,max=128"` | ||
} | ||
|
||
// UserKey represents the HTTP response format for a key | ||
type UserKey struct { | ||
// Name identifies the type of key (wrapping_key or sync_enc_seed) | ||
Name string `json:"name"` | ||
// EncryptedKey contains the encrypted key data as hex bytes | ||
EncryptedKey string `json:"encryptedKey"` | ||
// UpdatedAt is the timestamp when the key was last updated | ||
UpdatedAt time.Time `json:"updatedAt"` | ||
} | ||
|
||
// ToUserKey converts a UserKeyRequest to a UserKey | ||
func (r *UserKeyStoreRequest) ToDBUserKey(accountID uuid.UUID) (*datastore.DBUserKey, error) { | ||
encKey, err := hex.DecodeString(r.EncryptedKey) | ||
if err != nil { | ||
return nil, fmt.Errorf("invalid hex encoding: %w", err) | ||
} | ||
|
||
return &datastore.DBUserKey{ | ||
AccountID: accountID, | ||
Name: r.Name, | ||
EncryptedKey: encKey, | ||
UpdatedAt: time.Now().UTC(), | ||
}, nil | ||
} | ||
|
||
// FromDBUserKey converts a datastore.DBUserKey to a UserKey | ||
func FromDBUserKey(dbKey *datastore.DBUserKey) UserKey { | ||
return UserKey{ | ||
Name: dbKey.Name, | ||
EncryptedKey: hex.EncodeToString(dbKey.EncryptedKey), | ||
UpdatedAt: dbKey.UpdatedAt, | ||
} | ||
} | ||
|
||
type UserKeysController struct { | ||
ds *datastore.Datastore | ||
validate *validator.Validate | ||
} | ||
|
||
func NewUserKeysController(ds *datastore.Datastore) *UserKeysController { | ||
return &UserKeysController{ | ||
ds: ds, | ||
validate: validator.New(validator.WithRequiredStructEnabled()), | ||
} | ||
} | ||
|
||
func (uc *UserKeysController) Router(authMiddleware func(http.Handler) http.Handler) chi.Router { | ||
r := chi.NewRouter() | ||
r.Use(authMiddleware) | ||
|
||
r.Get("/", uc.ListKeys) | ||
r.Get("/{name}", uc.GetKey) | ||
r.Post("/", uc.SaveKey) | ||
|
||
return r | ||
} | ||
|
||
// @Summary List user keys | ||
// @Description Get all keys for the authenticated user | ||
// @Tags User keys | ||
// @Accept json | ||
// @Produce json | ||
// @Param Authorization header string true "Bearer + auth token" | ||
// @Param Brave-Key header string false "Brave services key (if one is configured)" | ||
// @Success 200 {array} UserKey | ||
// @Failure 401 {object} util.ErrorResponse | ||
// @Failure 403 {object} util.ErrorResponse | ||
// @Failure 500 {object} util.ErrorResponse | ||
// @Router /v2/keys [get] | ||
func (uc *UserKeysController) ListKeys(w http.ResponseWriter, r *http.Request) { | ||
session := r.Context().Value(middleware.ContextSession).(*datastore.Session) | ||
|
||
keys, err := uc.ds.GetUserKeys(session.AccountID) | ||
if err != nil { | ||
util.RenderErrorResponse(w, r, http.StatusInternalServerError, err) | ||
return | ||
} | ||
|
||
response := make([]UserKey, len(keys)) | ||
for i := range keys { | ||
response[i] = FromDBUserKey(&keys[i]) | ||
} | ||
|
||
render.JSON(w, r, response) | ||
} | ||
|
||
// @Summary Get user key | ||
// @Description Get a specific key by name for the authenticated user | ||
// @Tags User keys | ||
// @Accept json | ||
// @Produce json | ||
// @Param Authorization header string true "Bearer + auth token" | ||
// @Param Brave-Key header string false "Brave services key (if one is configured)" | ||
// @Param name path string true "Key name" | ||
// @Success 200 {object} UserKey | ||
// @Failure 401 {object} util.ErrorResponse | ||
// @Failure 403 {object} util.ErrorResponse | ||
// @Failure 404 {object} util.ErrorResponse | ||
// @Failure 500 {object} util.ErrorResponse | ||
// @Router /v2/keys/{name} [get] | ||
func (uc *UserKeysController) GetKey(w http.ResponseWriter, r *http.Request) { | ||
session := r.Context().Value(middleware.ContextSession).(*datastore.Session) | ||
name := chi.URLParam(r, keyNameURLParam) | ||
|
||
key, err := uc.ds.GetUserKey(session.AccountID, name) | ||
if err != nil { | ||
if errors.Is(err, datastore.ErrKeyNotFound) { | ||
util.RenderErrorResponse(w, r, http.StatusNotFound, err) | ||
return | ||
} | ||
util.RenderErrorResponse(w, r, http.StatusInternalServerError, err) | ||
return | ||
} | ||
|
||
render.JSON(w, r, FromDBUserKey(key)) | ||
} | ||
|
||
// @Summary Save user key | ||
// @Description Save a new key or update existing key for the authenticated user | ||
// @Tags User keys | ||
// @Accept json | ||
// @Produce json | ||
// @Param Authorization header string true "Bearer + auth token" | ||
// @Param Brave-Key header string false "Brave services key (if one is configured)" | ||
// @Param key body UserKeyStoreRequest true "Key to save" | ||
// @Success 204 "Key saved" | ||
// @Failure 400 {object} util.ErrorResponse | ||
// @Failure 401 {object} util.ErrorResponse | ||
// @Failure 403 {object} util.ErrorResponse | ||
// @Failure 500 {object} util.ErrorResponse | ||
// @Router /v2/keys [post] | ||
func (uc *UserKeysController) SaveKey(w http.ResponseWriter, r *http.Request) { | ||
session := r.Context().Value(middleware.ContextSession).(*datastore.Session) | ||
|
||
var requestData UserKeyStoreRequest | ||
if err := render.DecodeJSON(r.Body, &requestData); err != nil { | ||
util.RenderErrorResponse(w, r, http.StatusBadRequest, err) | ||
return | ||
} | ||
|
||
if err := uc.validate.Struct(requestData); err != nil { | ||
util.RenderErrorResponse(w, r, http.StatusBadRequest, err) | ||
return | ||
} | ||
|
||
dbKey, err := requestData.ToDBUserKey(session.AccountID) | ||
if err != nil { | ||
util.RenderErrorResponse(w, r, http.StatusBadRequest, err) | ||
return | ||
} | ||
|
||
if err := uc.ds.StoreUserKey(dbKey); err != nil { | ||
util.RenderErrorResponse(w, r, http.StatusInternalServerError, err) | ||
return | ||
} | ||
|
||
render.NoContent(w, r) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package datastore | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/google/uuid" | ||
"gorm.io/gorm" | ||
) | ||
|
||
var ErrKeyNotFound = errors.New("key not found") | ||
|
||
// DBUserKey represents a key in the database | ||
type DBUserKey struct { | ||
// AccountID is the UUID of the account that owns this key | ||
AccountID uuid.UUID `json:"-" gorm:"primaryKey"` | ||
// Name identifies the type of key (wrapping_key, sync_enc_seed, or sync_device_seed) | ||
Name string `json:"name" gorm:"primaryKey"` | ||
// EncryptedKey contains the encrypted key data as bytes | ||
EncryptedKey []byte `json:"encryptedKey"` | ||
// UpdatedAt is the timestamp when the key was last updated | ||
UpdatedAt time.Time `json:"updatedAt" gorm:"autoUpdateTime:false"` | ||
} | ||
|
||
// TableName overrides the default table name for DBUserKey | ||
func (DBUserKey) TableName() string { | ||
return "user_keys" | ||
} | ||
|
||
// StoreUserKey saves a user key to the database | ||
func (d *Datastore) StoreUserKey(key *DBUserKey) error { | ||
result := d.db.Save(key) | ||
if result.Error != nil { | ||
return fmt.Errorf("failed to store user key: %w", result.Error) | ||
} | ||
return nil | ||
} | ||
|
||
// GetUserKey retrieves a user key from the database | ||
func (d *Datastore) GetUserKey(accountID uuid.UUID, name string) (*DBUserKey, error) { | ||
var key DBUserKey | ||
result := d.db.Where("account_id = ? AND name = ?", accountID, name).First(&key) | ||
if result.Error != nil { | ||
if result.Error == gorm.ErrRecordNotFound { | ||
return nil, ErrKeyNotFound | ||
} | ||
return nil, fmt.Errorf("failed to retrieve user key: %w", result.Error) | ||
} | ||
return &key, nil | ||
} | ||
|
||
// GetUserKeys retrieves all keys for an account | ||
func (d *Datastore) GetUserKeys(accountID uuid.UUID) ([]DBUserKey, error) { | ||
var keys []DBUserKey | ||
result := d.db.Where("account_id = ?", accountID).Find(&keys) | ||
if result.Error != nil { | ||
return nil, fmt.Errorf("failed to retrieve user keys: %w", result.Error) | ||
} | ||
return keys, nil | ||
} |
Oops, something went wrong.