diff --git a/internal/services/configstore/action/secret.go b/internal/services/configstore/action/secret.go index 3fad289..9b6d370 100644 --- a/internal/services/configstore/action/secret.go +++ b/internal/services/configstore/action/secret.go @@ -66,30 +66,38 @@ func (h *ActionHandler) GetSecrets(ctx context.Context, parentType types.ConfigT return secrets, nil } -func (h *ActionHandler) CreateSecret(ctx context.Context, secret *types.Secret) (*types.Secret, error) { +func (h *ActionHandler) ValidateSecret(ctx context.Context, secret *types.Secret) error { if secret.Name == "" { - return nil, util.NewErrBadRequest(errors.Errorf("secret name required")) + return util.NewErrBadRequest(errors.Errorf("secret name required")) } if !util.ValidateName(secret.Name) { - return nil, util.NewErrBadRequest(errors.Errorf("invalid secret name %q", secret.Name)) + return util.NewErrBadRequest(errors.Errorf("invalid secret name %q", secret.Name)) } if secret.Type != types.SecretTypeInternal { - return nil, util.NewErrBadRequest(errors.Errorf("invalid secret type %q", secret.Type)) + return util.NewErrBadRequest(errors.Errorf("invalid secret type %q", secret.Type)) } switch secret.Type { case types.SecretTypeInternal: if len(secret.Data) == 0 { - return nil, util.NewErrBadRequest(errors.Errorf("empty secret data")) + return util.NewErrBadRequest(errors.Errorf("empty secret data")) } } if secret.Parent.Type == "" { - return nil, util.NewErrBadRequest(errors.Errorf("secret parent type required")) + return util.NewErrBadRequest(errors.Errorf("secret parent type required")) } if secret.Parent.ID == "" { - return nil, util.NewErrBadRequest(errors.Errorf("secret parentid required")) + return util.NewErrBadRequest(errors.Errorf("secret parentid required")) } if secret.Parent.Type != types.ConfigTypeProject && secret.Parent.Type != types.ConfigTypeProjectGroup { - return nil, util.NewErrBadRequest(errors.Errorf("invalid secret parent type %q", secret.Parent.Type)) + return util.NewErrBadRequest(errors.Errorf("invalid secret parent type %q", secret.Parent.Type)) + } + + return nil +} + +func (h *ActionHandler) CreateSecret(ctx context.Context, secret *types.Secret) (*types.Secret, error) { + if err := h.ValidateSecret(ctx, secret); err != nil { + return nil, err } var cgt *datamanager.ChangeGroupsUpdateToken @@ -144,6 +152,86 @@ func (h *ActionHandler) CreateSecret(ctx context.Context, secret *types.Secret) return secret, err } +type UpdateSecretRequest struct { + SecretName string + + Secret *types.Secret +} + +func (h *ActionHandler) UpdateSecret(ctx context.Context, req *UpdateSecretRequest) (*types.Secret, error) { + if err := h.ValidateSecret(ctx, req.Secret); err != nil { + return nil, err + } + + var curSecret *types.Secret + var cgt *datamanager.ChangeGroupsUpdateToken + // changegroup is the secret name + + // must do all the checks in a single transaction to avoid concurrent changes + err := h.readDB.Do(func(tx *db.Tx) error { + var err error + + parentID, err := h.readDB.ResolveConfigID(tx, req.Secret.Parent.Type, req.Secret.Parent.ID) + if err != nil { + return err + } + req.Secret.Parent.ID = parentID + + // check secret exists + curSecret, err = h.readDB.GetSecretByName(tx, req.Secret.Parent.ID, req.SecretName) + if err != nil { + return err + } + if curSecret == nil { + return util.NewErrBadRequest(errors.Errorf("secret with name %q for %s with id %q doesn't exists", req.SecretName, req.Secret.Parent.Type, req.Secret.Parent.ID)) + } + + if curSecret.Name != req.Secret.Name { + // check duplicate secret name + u, err := h.readDB.GetSecretByName(tx, req.Secret.Parent.ID, req.Secret.Name) + if err != nil { + return err + } + if u != nil { + return util.NewErrBadRequest(errors.Errorf("secret with name %q for %s with id %q already exists", req.Secret.Name, req.Secret.Parent.Type, req.Secret.Parent.ID)) + } + } + + // set/override ID that must be kept from the current secret + req.Secret.ID = curSecret.ID + + cgNames := []string{ + util.EncodeSha256Hex("secretname-" + req.Secret.ID), + util.EncodeSha256Hex("secretname-" + req.Secret.Name), + } + cgt, err = h.readDB.GetChangeGroupsUpdateTokens(tx, cgNames) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + secretj, err := json.Marshal(req.Secret) + if err != nil { + return nil, errors.Errorf("failed to marshal secret: %w", err) + } + actions := []*datamanager.Action{ + { + ActionType: datamanager.ActionTypePut, + DataType: string(types.ConfigTypeSecret), + ID: req.Secret.ID, + Data: secretj, + }, + } + + _, err = h.dm.WriteWal(ctx, actions, cgt) + return req.Secret, err +} + func (h *ActionHandler) DeleteSecret(ctx context.Context, parentType types.ConfigType, parentRef, secretName string) error { var secret *types.Secret diff --git a/internal/services/configstore/api/client.go b/internal/services/configstore/api/client.go index 608a1fc..5c0c231 100644 --- a/internal/services/configstore/api/client.go +++ b/internal/services/configstore/api/client.go @@ -226,6 +226,28 @@ func (c *Client) CreateProjectSecret(ctx context.Context, projectRef string, sec return resSecret, resp, err } +func (c *Client) UpdateProjectGroupSecret(ctx context.Context, projectGroupRef, secretName string, secret *types.Secret) (*Secret, *http.Response, error) { + pj, err := json.Marshal(secret) + if err != nil { + return nil, nil, err + } + + resSecret := new(Secret) + resp, err := c.getParsedResponse(ctx, "PUT", fmt.Sprintf("/projectgroups/%s/secrets/%s", url.PathEscape(projectGroupRef), secretName), nil, jsonContent, bytes.NewReader(pj), resSecret) + return resSecret, resp, err +} + +func (c *Client) UpdateProjectSecret(ctx context.Context, projectRef, secretName string, secret *types.Secret) (*Secret, *http.Response, error) { + pj, err := json.Marshal(secret) + if err != nil { + return nil, nil, err + } + + resSecret := new(Secret) + resp, err := c.getParsedResponse(ctx, "PUT", fmt.Sprintf("/projects/%s/secrets/%s", url.PathEscape(projectRef), secretName), nil, jsonContent, bytes.NewReader(pj), resSecret) + return resSecret, resp, err +} + func (c *Client) DeleteProjectGroupSecret(ctx context.Context, projectGroupRef, secretName string) (*http.Response, error) { return c.getResponse(ctx, "DELETE", fmt.Sprintf("/projectgroups/%s/secrets/%s", url.PathEscape(projectGroupRef), secretName), nil, jsonContent, nil) } diff --git a/internal/services/configstore/api/secret.go b/internal/services/configstore/api/secret.go index 895cfcb..88172e9 100644 --- a/internal/services/configstore/api/secret.go +++ b/internal/services/configstore/api/secret.go @@ -154,6 +154,51 @@ func (h *CreateSecretHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) } } +type UpdateSecretHandler struct { + log *zap.SugaredLogger + ah *action.ActionHandler +} + +func NewUpdateSecretHandler(logger *zap.Logger, ah *action.ActionHandler) *UpdateSecretHandler { + return &UpdateSecretHandler{log: logger.Sugar(), ah: ah} +} + +func (h *UpdateSecretHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + secretName := vars["secretname"] + + parentType, parentRef, err := GetConfigTypeRef(r) + if httpError(w, err) { + h.log.Errorf("err: %+v", err) + return + } + + var secret *types.Secret + d := json.NewDecoder(r.Body) + if err := d.Decode(&secret); err != nil { + httpError(w, util.NewErrBadRequest(err)) + return + } + + secret.Parent.Type = parentType + secret.Parent.ID = parentRef + + areq := &action.UpdateSecretRequest{ + SecretName: secretName, + Secret: secret, + } + secret, err = h.ah.UpdateSecret(ctx, areq) + if httpError(w, err) { + h.log.Errorf("err: %+v", err) + return + } + + if err := httpResponse(w, http.StatusOK, secret); err != nil { + h.log.Errorf("err: %+v", err) + } +} + type DeleteSecretHandler struct { log *zap.SugaredLogger ah *action.ActionHandler diff --git a/internal/services/configstore/configstore.go b/internal/services/configstore/configstore.go index a2d7bb9..b1245a1 100644 --- a/internal/services/configstore/configstore.go +++ b/internal/services/configstore/configstore.go @@ -134,6 +134,7 @@ func (s *Configstore) Run(ctx context.Context) error { secretsHandler := api.NewSecretsHandler(logger, s.ah, s.readDB) createSecretHandler := api.NewCreateSecretHandler(logger, s.ah) + updateSecretHandler := api.NewUpdateSecretHandler(logger, s.ah) deleteSecretHandler := api.NewDeleteSecretHandler(logger, s.ah) variablesHandler := api.NewVariablesHandler(logger, s.ah, s.readDB) @@ -190,6 +191,8 @@ func (s *Configstore) Run(ctx context.Context) error { apirouter.Handle("/projects/{projectref}/secrets", secretsHandler).Methods("GET") apirouter.Handle("/projectgroups/{projectgroupref}/secrets", createSecretHandler).Methods("POST") apirouter.Handle("/projects/{projectref}/secrets", createSecretHandler).Methods("POST") + apirouter.Handle("/projectgroups/{projectgroupref}/secrets/{secretname}", updateSecretHandler).Methods("PUT") + apirouter.Handle("/projects/{projectref}/secrets/{secretname}", updateSecretHandler).Methods("PUT") apirouter.Handle("/projectgroups/{projectgroupref}/secrets/{secretname}", deleteSecretHandler).Methods("DELETE") apirouter.Handle("/projects/{projectref}/secrets/{secretname}", deleteSecretHandler).Methods("DELETE")