mirror of
https://github.com/ferdzo/fs.git
synced 2026-04-04 20:36:25 +00:00
add admin user delete endpoint and service support
This commit is contained in:
@@ -38,6 +38,7 @@ Admin API (JSON):
|
|||||||
- `POST /_admin/v1/users`
|
- `POST /_admin/v1/users`
|
||||||
- `GET /_admin/v1/users`
|
- `GET /_admin/v1/users`
|
||||||
- `GET /_admin/v1/users/{accessKeyId}`
|
- `GET /_admin/v1/users/{accessKeyId}`
|
||||||
|
- `DELETE /_admin/v1/users/{accessKeyId}`
|
||||||
|
|
||||||
## Auth Setup
|
## Auth Setup
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ func (h *Handler) registerAdminRoutes() {
|
|||||||
r.Post("/users", h.handleAdminCreateUser)
|
r.Post("/users", h.handleAdminCreateUser)
|
||||||
r.Get("/users", h.handleAdminListUsers)
|
r.Get("/users", h.handleAdminListUsers)
|
||||||
r.Get("/users/{accessKeyId}", h.handleAdminGetUser)
|
r.Get("/users/{accessKeyId}", h.handleAdminGetUser)
|
||||||
|
r.Delete("/users/{accessKeyId}", h.handleAdminDeleteUser)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +156,18 @@ func (h *Handler) handleAdminGetUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusOK, resp)
|
writeJSON(w, http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleAdminDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.requireBootstrapAdmin(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
accessKeyID := chi.URLParam(r, "accessKeyId")
|
||||||
|
if err := h.authSvc.DeleteUser(accessKeyID); err != nil {
|
||||||
|
writeMappedAdminError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) requireBootstrapAdmin(w http.ResponseWriter, r *http.Request) bool {
|
func (h *Handler) requireBootstrapAdmin(w http.ResponseWriter, r *http.Request) bool {
|
||||||
authCtx, ok := auth.GetRequestContext(r.Context())
|
authCtx, ok := auth.GetRequestContext(r.Context())
|
||||||
if !ok || !authCtx.Authenticated {
|
if !ok || !authCtx.Authenticated {
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ import (
|
|||||||
type Store interface {
|
type Store interface {
|
||||||
GetAuthIdentity(accessKeyID string) (*models.AuthIdentity, error)
|
GetAuthIdentity(accessKeyID string) (*models.AuthIdentity, error)
|
||||||
PutAuthIdentity(identity *models.AuthIdentity) error
|
PutAuthIdentity(identity *models.AuthIdentity) error
|
||||||
|
DeleteAuthIdentity(accessKeyID string) error
|
||||||
ListAuthIdentities(limit int, after string) ([]models.AuthIdentity, string, error)
|
ListAuthIdentities(limit int, after string) ([]models.AuthIdentity, string, error)
|
||||||
GetAuthPolicy(accessKeyID string) (*models.AuthPolicy, error)
|
GetAuthPolicy(accessKeyID string) (*models.AuthPolicy, error)
|
||||||
PutAuthPolicy(policy *models.AuthPolicy) error
|
PutAuthPolicy(policy *models.AuthPolicy) error
|
||||||
|
DeleteAuthPolicy(accessKeyID string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateUserInput struct {
|
type CreateUserInput struct {
|
||||||
@@ -339,6 +341,40 @@ func (s *Service) GetUser(accessKeyID string) (*UserDetails, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeleteUser(accessKeyID string) error {
|
||||||
|
if !s.cfg.Enabled {
|
||||||
|
return ErrAuthNotEnabled
|
||||||
|
}
|
||||||
|
accessKeyID = strings.TrimSpace(accessKeyID)
|
||||||
|
if !validAccessKeyID.MatchString(accessKeyID) {
|
||||||
|
return fmt.Errorf("%w: invalid access key id", ErrInvalidUserInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap := strings.TrimSpace(s.cfg.BootstrapAccessKey)
|
||||||
|
if bootstrap != "" && accessKeyID == bootstrap {
|
||||||
|
return fmt.Errorf("%w: bootstrap user cannot be deleted", ErrInvalidUserInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.store.GetAuthIdentity(accessKeyID); err != nil {
|
||||||
|
if errors.Is(err, metadata.ErrAuthIdentityNotFound) {
|
||||||
|
return ErrUserNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.store.DeleteAuthIdentity(accessKeyID); err != nil {
|
||||||
|
if errors.Is(err, metadata.ErrAuthIdentityNotFound) {
|
||||||
|
return ErrUserNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.store.DeleteAuthPolicy(accessKeyID); err != nil && !errors.Is(err, metadata.ErrAuthPolicyNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func parsePolicyJSON(raw string) (*models.AuthPolicy, error) {
|
func parsePolicyJSON(raw string) (*models.AuthPolicy, error) {
|
||||||
policy := models.AuthPolicy{}
|
policy := models.AuthPolicy{}
|
||||||
if err := json.Unmarshal([]byte(raw), &policy); err != nil {
|
if err := json.Unmarshal([]byte(raw), &policy); err != nil {
|
||||||
|
|||||||
@@ -106,6 +106,38 @@ func TestCreateUserRejectsInvalidAccessKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeleteUser(t *testing.T) {
|
||||||
|
_, svc := newTestAuthService(t)
|
||||||
|
|
||||||
|
_, err := svc.CreateUser(CreateUserInput{
|
||||||
|
AccessKeyID: "delete-user",
|
||||||
|
SecretKey: "super-secret-1",
|
||||||
|
Policy: models.AuthPolicy{
|
||||||
|
Statements: []models.AuthPolicyStatement{
|
||||||
|
{Effect: "allow", Actions: []string{"s3:*"}, Bucket: "*", Prefix: "*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateUser returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := svc.DeleteUser("delete-user"); err != nil {
|
||||||
|
t.Fatalf("DeleteUser returned error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := svc.GetUser("delete-user"); !errors.Is(err, ErrUserNotFound) {
|
||||||
|
t.Fatalf("GetUser after delete error = %v, want ErrUserNotFound", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteBootstrapUserRejected(t *testing.T) {
|
||||||
|
_, svc := newTestAuthService(t)
|
||||||
|
|
||||||
|
if err := svc.DeleteUser("root-user"); !errors.Is(err, ErrInvalidUserInput) {
|
||||||
|
t.Fatalf("DeleteUser bootstrap error = %v, want ErrInvalidUserInput", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newTestAuthService(t *testing.T) (*metadata.MetadataHandler, *Service) {
|
func newTestAuthService(t *testing.T) (*metadata.MetadataHandler, *Service) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -154,6 +154,23 @@ func (h *MetadataHandler) PutAuthIdentity(identity *models.AuthIdentity) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *MetadataHandler) DeleteAuthIdentity(accessKeyID string) error {
|
||||||
|
accessKeyID = strings.TrimSpace(accessKeyID)
|
||||||
|
if accessKeyID == "" {
|
||||||
|
return errors.New("access key id is required")
|
||||||
|
}
|
||||||
|
return h.update(func(tx *bbolt.Tx) error {
|
||||||
|
bucket := tx.Bucket(authIdentitiesIndex)
|
||||||
|
if bucket == nil {
|
||||||
|
return errors.New("auth identities index not found")
|
||||||
|
}
|
||||||
|
if bucket.Get([]byte(accessKeyID)) == nil {
|
||||||
|
return fmt.Errorf("%w: %s", ErrAuthIdentityNotFound, accessKeyID)
|
||||||
|
}
|
||||||
|
return bucket.Delete([]byte(accessKeyID))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *MetadataHandler) GetAuthIdentity(accessKeyID string) (*models.AuthIdentity, error) {
|
func (h *MetadataHandler) GetAuthIdentity(accessKeyID string) (*models.AuthIdentity, error) {
|
||||||
accessKeyID = strings.TrimSpace(accessKeyID)
|
accessKeyID = strings.TrimSpace(accessKeyID)
|
||||||
if accessKeyID == "" {
|
if accessKeyID == "" {
|
||||||
@@ -205,6 +222,23 @@ func (h *MetadataHandler) PutAuthPolicy(policy *models.AuthPolicy) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *MetadataHandler) DeleteAuthPolicy(accessKeyID string) error {
|
||||||
|
accessKeyID = strings.TrimSpace(accessKeyID)
|
||||||
|
if accessKeyID == "" {
|
||||||
|
return errors.New("access key id is required")
|
||||||
|
}
|
||||||
|
return h.update(func(tx *bbolt.Tx) error {
|
||||||
|
bucket := tx.Bucket(authPoliciesIndex)
|
||||||
|
if bucket == nil {
|
||||||
|
return errors.New("auth policies index not found")
|
||||||
|
}
|
||||||
|
if bucket.Get([]byte(accessKeyID)) == nil {
|
||||||
|
return fmt.Errorf("%w: %s", ErrAuthPolicyNotFound, accessKeyID)
|
||||||
|
}
|
||||||
|
return bucket.Delete([]byte(accessKeyID))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *MetadataHandler) GetAuthPolicy(accessKeyID string) (*models.AuthPolicy, error) {
|
func (h *MetadataHandler) GetAuthPolicy(accessKeyID string) (*models.AuthPolicy, error) {
|
||||||
accessKeyID = strings.TrimSpace(accessKeyID)
|
accessKeyID = strings.TrimSpace(accessKeyID)
|
||||||
if accessKeyID == "" {
|
if accessKeyID == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user