diff --git a/.github/workflows/generate_schemas.yml b/.github/workflows/generate_schemas.yml index 675f53c1e..7eba684c4 100644 --- a/.github/workflows/generate_schemas.yml +++ b/.github/workflows/generate_schemas.yml @@ -64,6 +64,12 @@ jobs: path: ${{ github.workspace }}/support/schemas/* if-no-files-found: error + - name: Upload Go schemas artifact + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + name: schemas.go + path: ${{ github.workspace }}/languages/go/schema.go + - name: Upload java schemas artifact uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: diff --git a/.github/workflows/golang-release.yml b/.github/workflows/golang-release.yml new file mode 100644 index 000000000..6f41bcd3e --- /dev/null +++ b/.github/workflows/golang-release.yml @@ -0,0 +1,73 @@ +name: Go Release + +on: + workflow_dispatch: + inputs: + version_number: + description: "New Version" + required: true + +env: + GO111MODULE: on + GO_VERSION: "^1.18" + +jobs: + build_rust: + uses: ./.github/workflows/build-rust-cross-platform.yml + + generate-schemas: + uses: ./.github/workflows/generate_schemas.yml + + build: + name: Build + needs: + - build_rust + - generate-schemas + runs-on: ubuntu-22.04 + steps: + - name: Checkout Repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Setup Go environment + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Cache dependencies + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... + + release: + name: Release + needs: build + runs-on: ubuntu-22.04 + steps: + - name: Checkout Repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Setup Go environment + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Set release version + run: echo "VERSION=${{ github.event.inputs.version_number }}" >> $GITHUB_ENV + + - name: Install Goreleaser + run: go install github.com/goreleaser/goreleaser@v1.21.2 + + - name: Run Goreleaser + run: goreleaser release --rm-dist --skip-validate + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ env.VERSION }} diff --git a/.gitignore b/.gitignore index 38b074349..37d652453 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,5 @@ crates/bitwarden-napi/src-ts/bitwarden_client/schemas.ts languages/csharp/Bitwarden.Sdk/schemas.cs languages/js_webassembly/bitwarden_client/schemas.ts languages/python/BitwardenClient/schemas.py +languages/go/schema.go languages/java/src/main/java/com/bitwarden/sdk/schema diff --git a/languages/go/README.md b/languages/go/README.md new file mode 100644 index 000000000..e57badf7e --- /dev/null +++ b/languages/go/README.md @@ -0,0 +1,113 @@ +# Bitwarden SDK in Go + +This SDK is designed to interact with Bitwarden services in Go. It includes implementations for +managing projects and secrets, as well as a client interface to facilitate operations like login. + +## Prerequisites + +- Go installed +- C environment to run CGO + +## Installation + +Download the SDK files and place them in your Go project directory. + +## Table of Contents + +- [Initialization](#initialization) +- [Login](#login) +- [Projects](#projects) +- [Secrets](#secrets) +- [Close Client](#close-client) + +--- + +### Initialization + +To initialize the client, you need to import the SDK and create a new `BitwardenClient` instance. + +```go +import "github.com/bitwarden/sdk/languages/go" + +bitwardenClient, _ := sdk.NewBitwardenClient(&apiURL, &identityURL) +``` + +--- + +### Login + +To login using an access token: + +```go +apiKeyLogin, err := bitwardenClient.AccessTokenLogin(accessToken) +``` + +--- + +### Projects + +#### Create a Project + +```go +project, err := client.Projects.Create("organization_id", "project_name") +``` + +#### List Projects + +```go +projects, err := client.Projects.List("organization_id") +``` + +#### Update a Project + +```go +project, err := client.Projects.Update("project_id", "organization_id", "new_project_name") +``` + +#### Delete Projects + +```go +project, err := client.Projects.Delete([]string{"project_id_1", "project_id_2"}) +``` + +--- + +### Secrets + +#### Create a Secret + +```go +secret, err := client.Secrets.Create("key", "value", "note", "organization_id", []string{"project_id"}) +``` + +#### List Secrets + +```go +secrets, err := client.Secrets.List("organization_id") +``` + +#### Update a Secret + +```go +secret, err := client.Secrets.Update("secret_id", "new_key", "new_value", "new_note", "organization_id", []string{"project_id"}) +``` + +#### Delete Secrets + +```go +secret, err := client.Secrets.Delete([]string{"secret_id_1", "secret_id_2"}) +``` + +--- + +### Close Client + +To free up resources: + +```go +defer bitwardenClient.Close() +``` + +--- + +For more detailed information, refer to the code comments and method signatures. diff --git a/languages/go/bitwarden_client.go b/languages/go/bitwarden_client.go new file mode 100644 index 000000000..5e1108ce1 --- /dev/null +++ b/languages/go/bitwarden_client.go @@ -0,0 +1,63 @@ +package sdk + +import ( + "encoding/json" + + "github.com/bitwarden/sdk/languages/go/internal/cinterface" +) + +type BitwardenClient struct { + client cinterface.ClientPointer + lib cinterface.BitwardenLibrary + commandRunner CommandRunnerInterface + Projects ProjectsInterface + Secrets SecretsInterface +} + +func NewBitwardenClient(apiURL *string, identityURL *string) (*BitwardenClient, error) { + deviceType := DeviceType("SDK") + userAgent := "Bitwarden GOLANG-SDK" + clientSettings := ClientSettings{ + APIURL: apiURL, + IdentityURL: identityURL, + UserAgent: &userAgent, + DeviceType: &deviceType, + } + + settingsJSON, err := json.Marshal(clientSettings) + if err != nil { + return nil, err + } + + lib := cinterface.NewBitwardenLibrary() + client, err := lib.Init(string(settingsJSON)) + if err != nil { + return nil, err + } + runner := NewCommandRunner(client, lib) + + return &BitwardenClient{ + lib: lib, + client: client, + commandRunner: runner, + Projects: NewProjects(runner), + Secrets: NewSecrets(runner), + }, nil +} + +func (c *BitwardenClient) AccessTokenLogin(accessToken string) error { + req := AccessTokenLoginRequest{AccessToken: accessToken} + command := Command{AccessTokenLogin: &req} + + responseStr, err := c.commandRunner.RunCommand(command) + if err != nil { + return err + } + + var response APIKeyLoginResponse + return checkSuccessAndError(responseStr, &response) +} + +func (c *BitwardenClient) Close() { + c.lib.FreeMem(c.client) +} diff --git a/languages/go/command_runner.go b/languages/go/command_runner.go new file mode 100644 index 000000000..3f79f7149 --- /dev/null +++ b/languages/go/command_runner.go @@ -0,0 +1,37 @@ +package sdk + +import ( + "encoding/json" + + "github.com/bitwarden/sdk/languages/go/internal/cinterface" +) + +type CommandRunnerInterface interface { + RunCommand(command Command) (string, error) +} + +type CommandRunner struct { + client cinterface.ClientPointer + lib cinterface.BitwardenLibrary +} + +func NewCommandRunner(client cinterface.ClientPointer, lib cinterface.BitwardenLibrary) *CommandRunner { + return &CommandRunner{ + client: client, + lib: lib, + } +} + +func (c *CommandRunner) RunCommand(command Command) (string, error) { + commandJSON, err := json.Marshal(command) + if err != nil { + return "", err + } + + responseStr, err := c.lib.RunCommand(string(commandJSON), c.client) + if err != nil { + return "", err + } + + return responseStr, nil +} diff --git a/languages/go/example/example.go b/languages/go/example/example.go new file mode 100644 index 000000000..5935d8002 --- /dev/null +++ b/languages/go/example/example.go @@ -0,0 +1,86 @@ +package main + +import ( + "fmt" + "os" + + sdk "github.com/bitwarden/sdk/languages/go" + "github.com/gofrs/uuid" +) + +func main() { + apiURL := os.Getenv("API_URL") + identityURL := os.Getenv("IDENTITY_URL") + + bitwardenClient, _ := sdk.NewBitwardenClient(&apiURL, &identityURL) + + accessToken := os.Getenv("ACCESS_TOKEN") + organizationIDStr := os.Getenv("ORGANIZATION_ID") + projectName := os.Getenv("PROJECT_NAME") + + if projectName == "" { + projectName = "NewTestProject" // default value + } + + err := bitwardenClient.AccessTokenLogin(accessToken) + if err != nil { + panic(err) + } + + organizationID, err := uuid.FromString(organizationIDStr) + if err != nil { + panic(err) + } + + project, err := bitwardenClient.Projects.Create(organizationID.String(), projectName) + if err != nil { + panic(err) + } + fmt.Println(project) + projectID := project.ID + fmt.Println(projectID) + + if _, err = bitwardenClient.Projects.List(organizationID.String()); err != nil { + panic(err) + } + + if _, err = bitwardenClient.Projects.Get(projectID); err != nil { + panic(err) + } + + if _, err = bitwardenClient.Projects.Update(projectID, organizationID.String(), projectName+"2"); err != nil { + panic(err) + } + + key := "key" + value := "value" + note := "note" + + secret, err := bitwardenClient.Secrets.Create(key, value, note, organizationID.String(), []string{projectID}) + if err != nil { + panic(err) + } + secretID := secret.ID + + if _, err = bitwardenClient.Secrets.List(organizationID.String()); err != nil { + panic(err) + } + + if _, err = bitwardenClient.Secrets.Get(secretID); err != nil { + panic(err) + } + + if _, err = bitwardenClient.Secrets.Update(secretID, key, value, note, organizationID.String(), []string{projectID}); err != nil { + panic(err) + } + + if _, err = bitwardenClient.Secrets.Delete([]string{secretID}); err != nil { + panic(err) + } + + if _, err = bitwardenClient.Projects.Delete([]string{projectID}); err != nil { + panic(err) + } + + defer bitwardenClient.Close() +} diff --git a/languages/go/example/go.mod b/languages/go/example/go.mod new file mode 100644 index 000000000..bbde28fd5 --- /dev/null +++ b/languages/go/example/go.mod @@ -0,0 +1,10 @@ +module example + +replace github.com/bitwarden/sdk/languages/go => ../ + +go 1.20 + +require ( + github.com/bitwarden/sdk/languages/go v0.0.0-00010101000000-000000000000 + github.com/gofrs/uuid v4.4.0+incompatible +) diff --git a/languages/go/example/go.sum b/languages/go/example/go.sum new file mode 100644 index 000000000..c0ad68738 --- /dev/null +++ b/languages/go/example/go.sum @@ -0,0 +1,2 @@ +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= diff --git a/languages/go/go.mod b/languages/go/go.mod new file mode 100644 index 000000000..a9e125453 --- /dev/null +++ b/languages/go/go.mod @@ -0,0 +1,5 @@ +module github.com/bitwarden/sdk/languages/go + +go 1.18 + +require github.com/gofrs/uuid v4.4.0+incompatible diff --git a/languages/go/go.sum b/languages/go/go.sum new file mode 100644 index 000000000..c0ad68738 --- /dev/null +++ b/languages/go/go.sum @@ -0,0 +1,2 @@ +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= diff --git a/languages/go/internal/cinterface/bitwarden_library.go b/languages/go/internal/cinterface/bitwarden_library.go new file mode 100644 index 000000000..42327adb2 --- /dev/null +++ b/languages/go/internal/cinterface/bitwarden_library.go @@ -0,0 +1,55 @@ +package cinterface + +import ( + "fmt" + "unsafe" +) + +/* +#cgo LDFLAGS: -lbitwarden_c +#cgo linux LDFLAGS: -L/usr/local/lib -L/usr/lib -L ./lib +#cgo darwin LDFLAGS: -L/usr/local/lib -L/usr/lib -L ./lib +#include +typedef void* ClientPtr; +extern char* run_command(const char *command, ClientPtr client); +extern ClientPtr init(const char *clientSettings); +extern void free_mem(ClientPtr client); +*/ +import "C" + +type ClientPointer struct { + Pointer C.ClientPtr +} + +type BitwardenLibrary interface { + Init(clientSettings string) (ClientPointer, error) + FreeMem(client ClientPointer) + RunCommand(command string, client ClientPointer) (string, error) +} + +type BitwardenLibraryImpl struct{} + +func NewBitwardenLibrary() BitwardenLibrary { + return &BitwardenLibraryImpl{} +} + +func (b *BitwardenLibraryImpl) Init(clientSettings string) (ClientPointer, error) { + ptr := C.init(C.CString(clientSettings)) + if ptr == nil { + return ClientPointer{}, fmt.Errorf("initialization failed") + } + return ClientPointer{Pointer: ptr}, nil +} + +func (b *BitwardenLibraryImpl) FreeMem(client ClientPointer) { + C.free_mem(client.Pointer) +} + +func (b *BitwardenLibraryImpl) RunCommand(command string, client ClientPointer) (string, error) { + cstr := C.run_command(C.CString(command), client.Pointer) + if cstr == nil { + return "", fmt.Errorf("run command failed") + } + defer C.free(unsafe.Pointer(cstr)) + return C.GoString(cstr), nil +} diff --git a/languages/go/project.go b/languages/go/project.go new file mode 100644 index 000000000..24a30a7ac --- /dev/null +++ b/languages/go/project.go @@ -0,0 +1,107 @@ +package sdk + +type ProjectsInterface interface { + Create(organizationID string, name string) (*ProjectResponse, error) + List(organizationID string) (*ProjectsResponse, error) + Get(projectID string) (*ProjectResponse, error) + Update(projectID string, organizationID string, name string) (*ProjectResponse, error) + Delete(projectIDs []string) (*ProjectsDeleteResponse, error) +} + +type Projects struct { + CommandRunner CommandRunnerInterface +} + +func NewProjects(commandRunner CommandRunnerInterface) *Projects { + return &Projects{CommandRunner: commandRunner} +} + +func (p *Projects) Get(id string) (*ProjectResponse, error) { + command := Command{ + Projects: &ProjectsCommand{ + Get: &ProjectGetRequest{ + ID: id, + }, + }, + } + var response ProjectResponse + if err := p.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (p *Projects) Create(organizationID string, name string) (*ProjectResponse, error) { + command := Command{ + Projects: &ProjectsCommand{ + Create: &ProjectCreateRequest{ + OrganizationID: organizationID, + Name: name, + }, + }, + } + + var response ProjectResponse + if err := p.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (p *Projects) List(organizationID string) (*ProjectsResponse, error) { + command := Command{ + Projects: &ProjectsCommand{ + List: &ProjectsListRequest{ + OrganizationID: organizationID, + }, + }, + } + + var response ProjectsResponse + if err := p.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (p *Projects) Update(projectID, organizationID, name string) (*ProjectResponse, error) { + command := Command{ + Projects: &ProjectsCommand{ + Update: &ProjectPutRequest{ + ID: projectID, + OrganizationID: organizationID, + Name: name, + }, + }, + } + + var response ProjectResponse + if err := p.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (p *Projects) Delete(projectIDs []string) (*ProjectsDeleteResponse, error) { + command := Command{ + Projects: &ProjectsCommand{ + Delete: &ProjectsDeleteRequest{ + IDS: projectIDs, + }, + }, + } + + var response ProjectsDeleteResponse + if err := p.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (p *Projects) executeCommand(command Command, target interface{}) error { + responseStr, err := p.CommandRunner.RunCommand(command) + if err != nil { + return err + } + return checkSuccessAndError(responseStr, target) +} diff --git a/languages/go/secrets.go b/languages/go/secrets.go new file mode 100644 index 000000000..e6863c459 --- /dev/null +++ b/languages/go/secrets.go @@ -0,0 +1,114 @@ +package sdk + +type SecretsInterface interface { + Create(key, value, note string, organizationID string, projectIDs []string) (*SecretResponse, error) + List(organizationID string) (*SecretIdentifiersResponse, error) + Get(secretID string) (*SecretResponse, error) + Update(secretID string, key, value, note string, organizationID string, projectIDs []string) (*SecretResponse, error) + Delete(secretIDs []string) (*SecretsDeleteResponse, error) +} + +type Secrets struct { + CommandRunner CommandRunnerInterface +} + +func NewSecrets(commandRunner CommandRunnerInterface) *Secrets { + return &Secrets{CommandRunner: commandRunner} +} + +func (s *Secrets) executeCommand(command Command, target interface{}) error { + responseStr, err := s.CommandRunner.RunCommand(command) + if err != nil { + return err + } + return checkSuccessAndError(responseStr, target) +} + +func (s *Secrets) Create(key, value, note string, organizationID string, projectIDs []string) (*SecretResponse, error) { + command := Command{ + Secrets: &SecretsCommand{ + Create: &SecretCreateRequest{ + Key: key, + Value: value, + Note: note, + OrganizationID: organizationID, + ProjectIDS: projectIDs, + }, + }, + } + + var response SecretResponse + if err := s.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (s *Secrets) List(organizationID string) (*SecretIdentifiersResponse, error) { + command := Command{ + Secrets: &SecretsCommand{ + List: &SecretIdentifiersRequest{ + OrganizationID: organizationID, + }, + }, + } + + var response SecretIdentifiersResponse + if err := s.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (s *Secrets) Get(id string) (*SecretResponse, error) { + command := Command{ + Secrets: &SecretsCommand{ + Get: &SecretGetRequest{ + ID: id, + }, + }, + } + + var response SecretResponse + if err := s.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (s *Secrets) Update(id string, key, value, note string, organizationID string, projectIDs []string) (*SecretResponse, error) { + command := Command{ + Secrets: &SecretsCommand{ + Update: &SecretPutRequest{ + ID: id, + Key: key, + Value: value, + Note: note, + OrganizationID: organizationID, + ProjectIDS: projectIDs, + }, + }, + } + + var response SecretResponse + if err := s.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (s *Secrets) Delete(ids []string) (*SecretsDeleteResponse, error) { + command := Command{ + Secrets: &SecretsCommand{ + Delete: &SecretsDeleteRequest{ + IDS: ids, + }, + }, + } + + var response SecretsDeleteResponse + if err := s.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} diff --git a/languages/go/util.go b/languages/go/util.go new file mode 100644 index 000000000..01ab3578d --- /dev/null +++ b/languages/go/util.go @@ -0,0 +1,33 @@ +package sdk + +import ( + "encoding/json" + "fmt" +) + +func checkSuccessAndError(responseStr string, v interface{}) error { + var wrapper struct { + Success bool `json:"success"` + ErrorMessage *string `json:"errorMessage"` + Data *json.RawMessage `json:"data"` + } + + err := json.Unmarshal([]byte(responseStr), &wrapper) + if err != nil { + return fmt.Errorf("failed to unmarshal wrapper response: %v", err) + } + + if !wrapper.Success { + if wrapper.ErrorMessage != nil { + return fmt.Errorf("API error: %s", *wrapper.ErrorMessage) + } + return fmt.Errorf("API error: unknown") + } + + err = json.Unmarshal(*wrapper.Data, &v) + if err != nil { + return fmt.Errorf("failed to unmarshal response: %v", err) + } + + return nil +} diff --git a/support/scripts/schemas.ts b/support/scripts/schemas.ts index 7181d6010..98c0c39cb 100644 --- a/support/scripts/schemas.ts +++ b/support/scripts/schemas.ts @@ -70,6 +70,17 @@ async function main() { writeToFile("./languages/csharp/Bitwarden.Sdk/schemas.cs", csharp.lines); + const go = await quicktype({ + inputData, + lang: "go", + rendererOptions: { + package: "sdk", + "just-types-and-package": true, + }, + }); + + writeToFile("./languages/go/schema.go", go.lines); + const java = await quicktypeMultiFile({ inputData, lang: "java",