Skip to content

Commit

Permalink
add support for creating projects (#181)
Browse files Browse the repository at this point in the history
  • Loading branch information
maxlaverse authored Nov 5, 2024
1 parent 3bd2b62 commit 7082c27
Show file tree
Hide file tree
Showing 16 changed files with 503 additions and 79 deletions.
39 changes: 39 additions & 0 deletions docs/resources/project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "bitwarden_project Resource - terraform-provider-bitwarden"
subcategory: ""
description: |-
Manages a Project.
---

# bitwarden_project (Resource)

Manages a Project.

## Example Usage

```terraform
resource "bitwarden_project" "example" {
name = "Example Project"
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `name` (String) Name.

### Optional

- `id` (String) Identifier.
- `organization_id` (String) Identifier of the organization.

## Import

Import is supported using the following syntax:

```shell
$ terraform import bitwarden_project.example <project_id>
```
1 change: 1 addition & 0 deletions examples/resources/bitwarden_project/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$ terraform import bitwarden_project.example <project_id>
3 changes: 3 additions & 0 deletions examples/resources/bitwarden_project/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
resource "bitwarden_project" "example" {
name = "Example Project"
}
3 changes: 3 additions & 0 deletions internal/bitwarden/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ type PasswordManager interface {
}

type SecretsManager interface {
CreateProject(ctx context.Context, project models.Project) (*models.Project, error)
CreateSecret(ctx context.Context, secret models.Secret) (*models.Secret, error)
DeleteProject(ctx context.Context, project models.Project) error
DeleteSecret(ctx context.Context, secret models.Secret) error
EditProject(ctx context.Context, project models.Project) (*models.Project, error)
EditSecret(ctx context.Context, secret models.Secret) (*models.Secret, error)
GetProject(ctx context.Context, project models.Project) (*models.Project, error)
GetSecret(ctx context.Context, secret models.Secret) (*models.Secret, error)
Expand Down
90 changes: 84 additions & 6 deletions internal/bitwarden/embedded/secrets_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ import (
)

type SecretsManager interface {
CreateProject(ctx context.Context, project models.Project) (*models.Project, error)
CreateSecret(ctx context.Context, secret models.Secret) (*models.Secret, error)
DeleteProject(ctx context.Context, project models.Project) error
DeleteSecret(ctx context.Context, secret models.Secret) error
EditProject(ctx context.Context, project models.Project) (*models.Project, error)
EditSecret(ctx context.Context, secret models.Secret) (*models.Secret, error)
GetProject(ctx context.Context, project models.Project) (*models.Project, error)
GetSecret(ctx context.Context, secret models.Secret) (*models.Secret, error)
Expand Down Expand Up @@ -60,6 +63,33 @@ type secretsManager struct {
serverURL string
}

func (v *secretsManager) CreateProject(ctx context.Context, project models.Project) (*models.Project, error) {
if v.mainEncryptionKey == nil {
return nil, models.ErrLoggedOut
}

var resProject *models.Project

encProject, err := encryptProject(project, *v.mainEncryptionKey)
if err != nil {
return nil, fmt.Errorf("error encrypting project for creation: %w", err)
}
encProject.OrganizationID = v.mainOrganizationId

resEncProject, err := v.client.CreateProject(ctx, *encProject)

if err != nil {
return nil, fmt.Errorf("error creating project: %w", err)
}

resProject, err = decryptProject(*resEncProject, *v.mainEncryptionKey)
if err != nil {
return nil, fmt.Errorf("error decrypting project after creation: %w", err)
}

return resProject, nil
}

func (v *secretsManager) CreateSecret(ctx context.Context, secret models.Secret) (*models.Secret, error) {
if v.mainEncryptionKey == nil {
return nil, models.ErrLoggedOut
Expand Down Expand Up @@ -87,6 +117,15 @@ func (v *secretsManager) CreateSecret(ctx context.Context, secret models.Secret)
return resSecret, nil
}

func (v *secretsManager) DeleteProject(ctx context.Context, project models.Project) error {
err := v.client.DeleteProject(ctx, project.ID)
if err != nil {
return fmt.Errorf("error deleting project: %w", err)
}

return nil
}

func (v *secretsManager) DeleteSecret(ctx context.Context, secret models.Secret) error {
err := v.client.DeleteSecret(ctx, secret.ID)
if err != nil {
Expand All @@ -96,6 +135,32 @@ func (v *secretsManager) DeleteSecret(ctx context.Context, secret models.Secret)
return nil
}

func (v *secretsManager) EditProject(ctx context.Context, project models.Project) (*models.Project, error) {
if v.mainEncryptionKey == nil {
return nil, models.ErrLoggedOut
}

var resProject *models.Project

encProject, err := encryptProject(project, *v.mainEncryptionKey)
if err != nil {
return nil, fmt.Errorf("error encrypting project for edition: %w", err)
}

resEncProject, err := v.client.EditProject(ctx, *encProject)

if err != nil {
return nil, fmt.Errorf("error editing project: %w", err)
}

resProject, err = decryptProject(*resEncProject, *v.mainEncryptionKey)
if err != nil {
return nil, fmt.Errorf("error decrypting project after edition: %w", err)
}

return resProject, nil
}

func (v *secretsManager) EditSecret(ctx context.Context, secret models.Secret) (*models.Secret, error) {
if v.mainEncryptionKey == nil {
return nil, models.ErrLoggedOut
Expand Down Expand Up @@ -301,18 +366,31 @@ func decryptSecret[T SecretType](webapiSecret T, mainEncryptionKey symmetrickey.
}, nil
}

func decryptProject(webapiProject webapi.Project, mainEncryptionKey symmetrickey.Key) (*models.Project, error) {
projectName, err := decryptStringIfNotEmpty(webapiProject.Name, mainEncryptionKey)
func decryptProject(project models.Project, mainEncryptionKey symmetrickey.Key) (*models.Project, error) {
projectName, err := decryptStringIfNotEmpty(project.Name, mainEncryptionKey)
if err != nil {
return nil, fmt.Errorf("error decrypting project name: %w", err)
}

return &models.Project{
CreationDate: webapiProject.CreationDate,
ID: webapiProject.ID,
CreationDate: project.CreationDate,
ID: project.ID,
Name: projectName,
OrganizationID: project.OrganizationID,
RevisionDate: project.RevisionDate,
}, nil
}

func encryptProject(project models.Project, mainEncryptionKey symmetrickey.Key) (*models.Project, error) {
projectName, err := encryptAsStringIfNotEmpty(project.Name, mainEncryptionKey)
if err != nil {
return nil, fmt.Errorf("error encrypt project name: %w", err)
}

return &models.Project{
ID: project.ID,
Name: projectName,
OrganizationID: webapiProject.OrganizationID,
RevisionDate: webapiProject.RevisionDate,
OrganizationID: project.OrganizationID,
}, nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const (
ObjectCipherDetails ObjectType = "cipherDetails" // when creating attachment data
ObjectAttachmentFileUpload ObjectType = "attachment-fileUpload" // when creating attachment data
ObjectApiKey ObjectType = "api-key"
ObjectProject ObjectType = "project"
ObjectSecret ObjectType = "secret"
)

Expand Down
13 changes: 8 additions & 5 deletions internal/bitwarden/models/secrets_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ type Secret struct {
}

type Project struct {
ID string `json:"id"`
OrganizationID string `json:"organizationId"`
Name string `json:"name"`
CreationDate time.Time `json:"creationDate"`
RevisionDate time.Time `json:"revisionDate"`
ID string `json:"id,omitempty"`
OrganizationID string `json:"organizationId,omitempty"`
Name string `json:"name,omitempty"`
CreationDate time.Time `json:"creationDate,omitempty"`
RevisionDate time.Time `json:"revisionDate,omitempty"`
Read bool `json:"read,omitempty"`
Write bool `json:"write,omitempty"`
Object string `json:"object,omitempty"`
}
48 changes: 43 additions & 5 deletions internal/bitwarden/webapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,26 @@ type Client interface {
CreateObjectAttachmentData(ctx context.Context, itemId, attachmentId string, data []byte) error
CreateOrganization(ctx context.Context, req CreateOrganizationRequest) (*CreateOrganizationResponse, error)
CreateOrgCollection(ctx context.Context, orgId string, req OrganizationCreationRequest) (*Collection, error)
CreateProject(ctx context.Context, project models.Project) (*models.Project, error)
CreateSecret(ctx context.Context, secret models.Secret) (*Secret, error)
DeleteFolder(ctx context.Context, objID string) error
DeleteObject(ctx context.Context, objID string) error
DeleteObjectAttachment(ctx context.Context, itemId, attachmentId string) error
DeleteOrgCollection(ctx context.Context, orgID, collectionID string) error
DeleteProject(ctx context.Context, projectId string) error
DeleteSecret(ctx context.Context, secretId string) error
EditFolder(ctx context.Context, obj Folder) (*Folder, error)
EditObject(context.Context, models.Object) (*models.Object, error)
EditOrgCollection(ctx context.Context, orgId, objId string, obj OrganizationCreationRequest) (*Collection, error)
EditProject(context.Context, models.Project) (*models.Project, error)
EditSecret(ctx context.Context, secret models.Secret) (*Secret, error)
GetAPIKey(ctx context.Context, username, password string, kdfConfig models.KdfConfiguration) (*ApiKey, error)
GetCollections(ctx context.Context, orgID string) ([]CollectionResponseItem, error)
GetContentFromURL(ctx context.Context, url string) ([]byte, error)
GetObjectAttachment(ctx context.Context, itemId, attachmentId string) (*models.Attachment, error)
GetProfile(context.Context) (*Profile, error)
GetProject(ctx context.Context, projectId string) (*Project, error)
GetProjects(ctx context.Context, orgId string) ([]Project, error)
GetProject(ctx context.Context, projectId string) (*models.Project, error)
GetProjects(ctx context.Context, orgId string) ([]models.Project, error)
GetSecret(ctx context.Context, secretId string) (*Secret, error)
GetSecrets(ctx context.Context, orgId string) ([]SecretSummary, error)
LoginWithAccessToken(ctx context.Context, clientId, clientSecret string) (*MachineTokenResponse, error)
Expand Down Expand Up @@ -166,6 +169,18 @@ func (c *client) CreateOrgCollection(ctx context.Context, orgId string, req Orga
return doRequest[Collection](ctx, c.httpClient, httpReq)
}

func (c *client) CreateProject(ctx context.Context, project models.Project) (*models.Project, error) {
projectCreationRequest := CreateProjectRequest{
Name: project.Name,
}
httpReq, err := c.prepareRequest(ctx, "POST", fmt.Sprintf("%s/api/organizations/%s/projects", c.serverURL, project.OrganizationID), projectCreationRequest)
if err != nil {
return nil, fmt.Errorf("error preparing secret creation request: %w", err)
}

return doRequest[models.Project](ctx, c.httpClient, httpReq)
}

func (c *client) CreateSecret(ctx context.Context, secret models.Secret) (*Secret, error) {
cipherCreationRequest := CreateSecretRequest{
Key: secret.Key,
Expand Down Expand Up @@ -221,6 +236,17 @@ func (c *client) DeleteOrgCollection(ctx context.Context, orgID, collectionID st
return err
}

func (c *client) DeleteProject(ctx context.Context, projectId string) error {
IDs := []string{projectId}
httpReq, err := c.prepareRequest(ctx, "POST", fmt.Sprintf("%s/api/projects/delete", c.serverURL), IDs)
if err != nil {
return fmt.Errorf("error preparing project deletion request: %w", err)
}

_, err = doRequest[[]byte](ctx, c.httpClient, httpReq)
return err
}

func (c *client) DeleteSecret(ctx context.Context, secretId string) error {
IDs := []string{secretId}
httpReq, err := c.prepareRequest(ctx, "POST", fmt.Sprintf("%s/api/secrets/delete", c.serverURL), IDs)
Expand Down Expand Up @@ -267,6 +293,18 @@ func (c *client) EditOrgCollection(ctx context.Context, orgId, objId string, obj
return doRequest[Collection](ctx, c.httpClient, req)
}

func (c *client) EditProject(ctx context.Context, project models.Project) (*models.Project, error) {
projectEditionRequest := CreateProjectRequest{
Name: project.Name,
}
httpReq, err := c.prepareRequest(ctx, "PUT", fmt.Sprintf("%s/api/projects/%s", c.serverURL, project.ID), projectEditionRequest)
if err != nil {
return nil, fmt.Errorf("error preparing secret edition request: %w", err)
}

return doRequest[models.Project](ctx, c.httpClient, httpReq)
}

func (c *client) EditSecret(ctx context.Context, secret models.Secret) (*Secret, error) {
cipherCreationRequest := CreateSecretRequest{
Key: secret.Key,
Expand Down Expand Up @@ -334,16 +372,16 @@ func (c *client) GetProfile(ctx context.Context) (*Profile, error) {
return doRequest[Profile](ctx, c.httpClient, httpReq)
}

func (c *client) GetProject(ctx context.Context, projectId string) (*Project, error) {
func (c *client) GetProject(ctx context.Context, projectId string) (*models.Project, error) {
httpReq, err := c.prepareRequest(ctx, "GET", fmt.Sprintf("%s/api/projects/%s", c.serverURL, projectId), nil)
if err != nil {
return nil, fmt.Errorf("error preparing project retrieval request: %w", err)
}

return doRequest[Project](ctx, c.httpClient, httpReq)
return doRequest[models.Project](ctx, c.httpClient, httpReq)
}

func (c *client) GetProjects(ctx context.Context, orgId string) ([]Project, error) {
func (c *client) GetProjects(ctx context.Context, orgId string) ([]models.Project, error) {
httpReq, err := c.prepareRequest(ctx, "GET", fmt.Sprintf("%s/api/organizations/%s/projects", c.serverURL, orgId), nil)
if err != nil {
return nil, fmt.Errorf("error preparing projects retrieval request: %w", err)
Expand Down
43 changes: 18 additions & 25 deletions internal/bitwarden/webapi/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,32 +177,21 @@ type MachineTokenEncryptedPayload struct {
EncryptionKey string `json:"encryptionKey"`
}

type Project struct {
ID string `json:"id"`
OrganizationID string `json:"organizationId"`
Name string `json:"name"`
CreationDate time.Time `json:"creationDate"`
RevisionDate time.Time `json:"revisionDate"`
Read bool `json:"read"`
Write bool `json:"write"`
Object string `json:"object"`
}

type Projects struct {
Data []Project `json:"data"`
ContinuationToken *string `json:"continuationToken"`
Object string `json:"object"`
Data []models.Project `json:"data"`
ContinuationToken *string `json:"continuationToken"`
Object string `json:"object"`
}

type SecretSummary struct {
ID string `json:"id"`
OrganizationID string `json:"organizationId"`
Key string `json:"key"`
CreationDate time.Time `json:"creationDate"`
RevisionDate time.Time `json:"revisionDate"`
Projects []Project `json:"projects"`
Read bool `json:"read"`
Write bool `json:"write"`
ID string `json:"id"`
OrganizationID string `json:"organizationId"`
Key string `json:"key"`
CreationDate time.Time `json:"creationDate"`
RevisionDate time.Time `json:"revisionDate"`
Projects []models.Project `json:"projects"`
Read bool `json:"read"`
Write bool `json:"write"`
}

type Secret struct {
Expand All @@ -213,9 +202,9 @@ type Secret struct {
}

type SecretsWithProjectsList struct {
Secrets []SecretSummary `json:"secrets"`
Projects []Project `json:"projects"`
Object string `json:"object"`
Secrets []SecretSummary `json:"secrets"`
Projects []models.Project `json:"projects"`
Object string `json:"object"`
}

type CreateSecretRequest struct {
Expand All @@ -226,6 +215,10 @@ type CreateSecretRequest struct {
AccessPoliciesRequests *AccessPoliciesRequests `json:"accessPoliciesRequests,omitempty"`
}

type CreateProjectRequest struct {
Name string `json:"name"`
}

type AccessPoliciesRequests struct {
UserAccessPolicyRequests []interface{} `json:"userAccessPolicyRequests"`
GroupAccessPolicyRequests []interface{} `json:"groupAccessPolicyRequests"`
Expand Down
Loading

0 comments on commit 7082c27

Please sign in to comment.