diff --git a/.github/workflows/build-java.yml b/.github/workflows/build-java.yml new file mode 100644 index 000000000..e500424dd --- /dev/null +++ b/.github/workflows/build-java.yml @@ -0,0 +1,68 @@ +name: Build Java SDK + +on: + pull_request: + branches: + - master + +jobs: + generate_schemas: + uses: ./.github/workflows/generate_schemas.yml + + build_rust: + uses: ./.github/workflows/build-rust-cross-platform.yml + + build_java: + name: Build Java + runs-on: ubuntu-22.04 + needs: + - generate_schemas + - build_rust + + steps: + - name: Checkout Repository + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + + - name: Download Java schemas artifact + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: sdk-schemas-java + path: languages/java/src/main/java/bit/sdk/schema/ + + - name: Setup Java + uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0 + with: + distribution: temurin + java-version: 17 + + - name: Download x86_64-apple-darwin files + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: libbitwarden_c_files-x86_64-apple-darwin + path: languages/java/src/main/resources/darwin-x64 + + - name: Download aarch64-apple-darwin files + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: libbitwarden_c_files-aarch64-apple-darwin + path: languages/java/src/main/resources/darwin-aarch64 + + - name: Download x86_64-unknown-linux-gnu files + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: libbitwarden_c_files-x86_64-unknown-linux-gnu + path: languages/java/src/main/resources/ubuntu-x64 + + - name: Download x86_64-pc-windows-msvc files + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: libbitwarden_c_files-x86_64-pc-windows-msvc + path: languages/java/src/main/resources/windows-x64 + + - name: Publish Maven + uses: gradle/gradle-build-action@b5126f31dbc19dd434c3269bf8c28c315e121da2 # v2.8.1 + with: + arguments: publish + build-root-directory: languages/java + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/generate_schemas.yml b/.github/workflows/generate_schemas.yml index 5f5742da0..675f53c1e 100644 --- a/.github/workflows/generate_schemas.yml +++ b/.github/workflows/generate_schemas.yml @@ -63,3 +63,10 @@ jobs: name: sdk-schemas-json path: ${{ github.workspace }}/support/schemas/* if-no-files-found: error + + - name: Upload java schemas artifact + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + with: + name: sdk-schemas-java + path: ${{ github.workspace }}/languages/java/src/main/java/com/bitwarden/sdk/schema/* + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 3f459493b..38b074349 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ 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/java/src/main/java/com/bitwarden/sdk/schema diff --git a/.vscode/tasks.json b/.vscode/tasks.json index fa97b8fc7..71400c06e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -46,6 +46,17 @@ "options": { "cwd": "${workspaceFolder}/languages/python" } + }, + { + "label": "buildJava", + "type": "shell", + "command": "gradle", + "args": ["build"], + "dependsOrder": "sequence", + "dependsOn": ["rust: bitwarden-c build"], + "options": { + "cwd": "${workspaceFolder}/languages/java" + } } ] } diff --git a/languages/java/.gitignore b/languages/java/.gitignore new file mode 100644 index 000000000..b5b6c5697 --- /dev/null +++ b/languages/java/.gitignore @@ -0,0 +1,3 @@ +target +.gradle +src/main/resources diff --git a/languages/java/README.md b/languages/java/README.md new file mode 100644 index 000000000..06f1ffec3 --- /dev/null +++ b/languages/java/README.md @@ -0,0 +1,73 @@ +# Bitwarden Secrets Manager SDK + +Java bindings for interacting with the [Bitwarden Secrets Manager]. This is a beta release and might be missing some +functionality. + +## Create access token + +Review the help documentation on [Access Tokens] + +## Usage code snippets + +### Create new Bitwarden client + +```java +BitwardenSettings bitwardenSettings = new BitwardenSettings(); +bitwardenSettings.setApiUrl("https://api.bitwarden.com"); +bitwardenSettings.setIdentityUrl("https://identity.bitwarden.com"); +BitwardenClient bitwardenClient = new BitwardenClient(bitwardenSettings); +bitwardenClient.accessTokenLogin(""); +``` + +### Create new project + +```java +UUID organizationId = UUID.fromString(""); +var projectResponse = bitwardenClient.projects().create(organizationId, "TestProject"); +``` + +### List all projects + +```java +var projectsResponse = bitwardenClient.projects().list(organizationId); +``` + +### Update project + +```java +UUID projectId = projectResponse.getID(); +projectResponse = bitwardenClient.projects().get(projectId); +projectResponse = bitwardenClient.projects.update(projectId, organizationId, "TestProjectUpdated"); +``` + +### Add new secret + +```java +String key = "key"; +String value = "value"; +String note = "note"; +var secretResponse = bitwardenClient.secrets().create(key, value, note, organizationId, new UUID[]{projectId}); +UUID secretId = secretResponse.getID(); +``` + +### Update secret + +```java +bitwardenClient.secrets().update(secretId, key2, value2, note2, organizationId, new UUID[]{projectId}); +``` + +### List secrets + +```java +var secretIdentifiersResponse secretIdentifiersResponse = bitwardenClient.secrets().list(organizationId); +``` + +# Delete secret or project + +```java +bitwardenClient.secrets().delete(new UUID[]{secretId}); +bitwardenClient.projects().delete(new UUID[]{projectId}); +``` + +[Access Tokens]: https://bitwarden.com/help/access-tokens/ +[Bitwarden Secrets Manager]: https://bitwarden.com/products/secrets-manager/ \ No newline at end of file diff --git a/languages/java/build.gradle b/languages/java/build.gradle new file mode 100644 index 000000000..dc17386c4 --- /dev/null +++ b/languages/java/build.gradle @@ -0,0 +1,90 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + id 'java-library' + id 'maven-publish' +} + +repositories { + mavenLocal() + maven { + url = uri('https://repo.maven.apache.org/maven2/') + } + + dependencies { + api 'com.fasterxml.jackson.core:jackson-core:2.9.10' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.10' + api 'net.java.dev.jna:jna-platform:5.12.1' + } + + description = 'BitwardenSDK' + java.sourceCompatibility = JavaVersion.VERSION_1_8 + + publishing { + publications { + maven(MavenPublication) { + groupId = 'com.bitwarden' + artifactId = 'sdk' + + // Determine the version from the git history. + // + // PRs: use the branch name. + // Master: Grab it from `crates/bitwarden/Cargo.toml` + + def branchName = "git branch --show-current".execute().text.trim() + + if (branchName == "master") { + def content = ['grep', '-o', '^version = ".*"', '../../crates/bitwarden/Cargo.toml'].execute().text.trim() + def match = ~/version = "(.*)"/ + def matcher = match.matcher(content) + matcher.find() + + version = "${matcher.group(1)}-SNAPSHOT" + } else { + // branchName-SNAPSHOT + version = "${branchName.replaceAll('/', '-')}-SNAPSHOT" + } + + afterEvaluate { + from components.java + } + } + } + repositories { + maven { + name = "GitHubPackages" + url = "https://maven.pkg.github.com/bitwarden/sdk" + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } + } +} + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +tasks.withType(Javadoc) { + options.encoding = 'UTF-8' +} + +// Gradle build requires GitHub workflow to copy native library to resources +// Uncomment copyNativeLib and jar tasks to use the local build (modify architecture if needed) +//tasks.register('copyNativeLib', Copy) { +// delete 'src/main/resources/darwin-aarch64' +// from '../../target/debug' +// include '*libbitwarden_c*.dylib' +// include '*libbitwarden_c*.so' +// include '*bitwarden_c*.dll' +// into 'src/main/resources/darwin-aarch64' +//} +// +//jar { +// dependsOn tasks.named("copyNativeLib").get() +// from 'src/main/resources' +//} diff --git a/languages/java/gradle/wrapper/gradle-wrapper.jar b/languages/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..7f93135c4 Binary files /dev/null and b/languages/java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/languages/java/gradle/wrapper/gradle-wrapper.properties b/languages/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..ac72c34e8 --- /dev/null +++ b/languages/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/languages/java/gradlew b/languages/java/gradlew new file mode 100755 index 000000000..0adc8e1a5 --- /dev/null +++ b/languages/java/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/languages/java/gradlew.bat b/languages/java/gradlew.bat new file mode 100644 index 000000000..6689b85be --- /dev/null +++ b/languages/java/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/languages/java/settings.gradle b/languages/java/settings.gradle new file mode 100644 index 000000000..961795f50 --- /dev/null +++ b/languages/java/settings.gradle @@ -0,0 +1,5 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = 'BitwardenSDK' diff --git a/languages/java/src/main/java/com/bitwarden/sdk/BitwardenClient.java b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenClient.java new file mode 100644 index 000000000..9e1948184 --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenClient.java @@ -0,0 +1,87 @@ +package com.bitwarden.sdk; + +import com.bitwarden.sdk.schema.*; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +import java.util.function.Function; + +public class BitwardenClient implements AutoCloseable { + + private final Pointer client; + + private final BitwardenLibrary library; + + private final CommandRunner commandRunner; + + private boolean isClientOpen; + + private final ProjectsClient projects; + + private final SecretsClient secrets; + + public BitwardenClient(BitwardenSettings bitwardenSettings) { + ClientSettings clientSettings = new ClientSettings(); + clientSettings.setAPIURL(bitwardenSettings.getApiUrl()); + clientSettings.setIdentityURL(bitwardenSettings.getIdentityUrl()); + clientSettings.setDeviceType(DeviceType.SDK); + clientSettings.setUserAgent("Bitwarden JAVA-SDK"); + + library = Native.load("bitwarden_c", BitwardenLibrary.class); + + try { + client = library.init(Converter.ClientSettingsToJsonString(clientSettings)); + } catch (JsonProcessingException e) { + throw new BitwardenClientException("Error while processing client settings"); + } + + commandRunner = new CommandRunner(library, client); + projects = new ProjectsClient(commandRunner); + secrets = new SecretsClient(commandRunner); + isClientOpen = true; + } + + static Function throwingFunctionWrapper(ThrowingFunction throwingFunction) { + + return i -> { + try { + return throwingFunction.accept(i); + } catch (Exception ex) { + throw new BitwardenClientException("Response deserialization failed"); + } + }; + } + + public APIKeyLoginResponse accessTokenLogin(String accessToken) { + Command command = new Command(); + AccessTokenLoginRequest accessTokenLoginRequest = new AccessTokenLoginRequest(); + accessTokenLoginRequest.setAccessToken(accessToken); + command.setAccessTokenLogin(accessTokenLoginRequest); + + ResponseForAPIKeyLoginResponse response = commandRunner.runCommand(command, + throwingFunctionWrapper(Converter::ResponseForAPIKeyLoginResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Login failed"); + } + + return response.getData(); + } + + public ProjectsClient projects() { + return projects; + } + + public SecretsClient secrets() { + return secrets; + } + + @Override + public void close() { + if (isClientOpen) { + library.free_mem(client); + isClientOpen = false; + } + } +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/BitwardenClientException.java b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenClientException.java new file mode 100644 index 000000000..7b6025be8 --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenClientException.java @@ -0,0 +1,8 @@ +package com.bitwarden.sdk; + +public class BitwardenClientException extends RuntimeException { + + public BitwardenClientException(String message) { + super(message); + } +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/BitwardenLibrary.java b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenLibrary.java new file mode 100644 index 000000000..73d81452c --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenLibrary.java @@ -0,0 +1,13 @@ +package com.bitwarden.sdk; + +import com.sun.jna.Library; +import com.sun.jna.Pointer; + +public interface BitwardenLibrary extends Library { + + Pointer init(String clientSettings); + + void free_mem(Pointer client); + + String run_command(String command, Pointer client); +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/BitwardenSettings.java b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenSettings.java new file mode 100644 index 000000000..7112297b4 --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenSettings.java @@ -0,0 +1,32 @@ +package com.bitwarden.sdk; + +public class BitwardenSettings { + + private String apiUrl; + + private String identityUrl; + + public BitwardenSettings() { + } + + public BitwardenSettings(String apiUrl, String identityUrl) { + this.apiUrl = apiUrl; + this.identityUrl = identityUrl; + } + + public String getApiUrl() { + return apiUrl; + } + + public void setApiUrl(String apiUrl) { + this.apiUrl = apiUrl; + } + + public String getIdentityUrl() { + return identityUrl; + } + + public void setIdentityUrl(String identityUrl) { + this.identityUrl = identityUrl; + } +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/CommandRunner.java b/languages/java/src/main/java/com/bitwarden/sdk/CommandRunner.java new file mode 100644 index 000000000..11a100814 --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/CommandRunner.java @@ -0,0 +1,45 @@ +package com.bitwarden.sdk; + +import com.bitwarden.sdk.schema.Command; +import com.bitwarden.sdk.schema.Converter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sun.jna.Pointer; + +import java.io.IOException; +import java.util.function.Function; + +class CommandRunner { + + private final BitwardenLibrary library; + + private final Pointer client; + + CommandRunner(BitwardenLibrary library, Pointer client) { + this.library = library; + this.client = client; + } + + T runCommand(Command command, Function deserializer) { + String response = null; + + try { + response = library.run_command(commandToString(command), client); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return deserializer.apply(response); + } + + private String commandToString(Command command) throws IOException { + // Removes null properties from the generated converter output to avoid command errors + String inputJson = Converter.CommandToJsonString(command); + + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + Object inputObject = mapper.readValue(inputJson, Object.class); + return mapper.writeValueAsString(inputObject); + } +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/ExampleProgram.java b/languages/java/src/main/java/com/bitwarden/sdk/ExampleProgram.java new file mode 100644 index 000000000..f3ad53036 --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/ExampleProgram.java @@ -0,0 +1,42 @@ +package com.bitwarden.sdk; + +import com.bitwarden.sdk.schema.*; + +import java.util.UUID; + +public class ExampleProgram { + + public static void main(String[] args) { + BitwardenSettings bitwardenSettings = new BitwardenSettings(); + bitwardenSettings.setApiUrl("https://api.bitwarden.com"); + bitwardenSettings.setIdentityUrl("https://identity.bitwarden.com"); + + try (BitwardenClient bitwardenClient = new BitwardenClient(bitwardenSettings)) { + APIKeyLoginResponse apiKeyLoginResponse = bitwardenClient.accessTokenLogin(""); + + UUID organizationId = UUID.fromString(""); + ProjectResponse projectResponse = bitwardenClient.projects().create(organizationId, "NewTestProject"); + UUID projectId = projectResponse.getID(); + ProjectsResponse projectsResponse = bitwardenClient.projects().list(organizationId); + projectResponse = bitwardenClient.projects().get(projectId); + projectResponse = bitwardenClient.projects().update(projectId, organizationId, "NewTestProject2"); + + String key = "key"; + String value = "value"; + String note = "note"; + SecretResponse secretResponse = bitwardenClient.secrets().create(key, value, note, organizationId, + new UUID[]{projectId}); + UUID secretId = secretResponse.getID(); + SecretIdentifiersResponse secretIdentifiersResponse = bitwardenClient.secrets().list(organizationId); + secretResponse = bitwardenClient.secrets().get(secretId); + key = "key2"; + value = "value2"; + note = "note2"; + secretResponse = bitwardenClient.secrets().update(secretId, key, value, note, organizationId, + new UUID[]{projectId}); + + SecretsDeleteResponse secretsDeleteResponse = bitwardenClient.secrets().delete(new UUID[]{secretId}); + ProjectsDeleteResponse projectsDeleteResponse = bitwardenClient.projects().delete(new UUID[]{projectId}); + } + } +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/ProjectsClient.java b/languages/java/src/main/java/com/bitwarden/sdk/ProjectsClient.java new file mode 100644 index 000000000..73b87440c --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/ProjectsClient.java @@ -0,0 +1,109 @@ +package com.bitwarden.sdk; + +import com.bitwarden.sdk.schema.*; + +import java.util.UUID; + +public class ProjectsClient { + + private final CommandRunner commandRunner; + + ProjectsClient(CommandRunner commandRunner) { + this.commandRunner = commandRunner; + } + + public ProjectResponse get(UUID id) { + Command command = new Command(); + ProjectsCommand projectsCommand = new ProjectsCommand(); + ProjectGetRequest projectGetRequest = new ProjectGetRequest(); + projectGetRequest.setID(id); + projectsCommand.setGet(projectGetRequest); + command.setProjects(projectsCommand); + + ResponseForProjectResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForProjectResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Project not found"); + } + + return response.getData(); + } + + public ProjectResponse create(UUID organizationId, String name) { + Command command = new Command(); + ProjectsCommand projectsCommand = new ProjectsCommand(); + ProjectCreateRequest projectCreateRequest = new ProjectCreateRequest(); + projectCreateRequest.setOrganizationID(organizationId); + projectCreateRequest.setName(name); + projectsCommand.setCreate(projectCreateRequest); + command.setProjects(projectsCommand); + + ResponseForProjectResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForProjectResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Project create failed"); + } + + return response.getData(); + } + + public ProjectResponse update(UUID id, UUID organizationId, String name) { + Command command = new Command(); + ProjectsCommand projectsCommand = new ProjectsCommand(); + ProjectPutRequest projectPutRequest = new ProjectPutRequest(); + projectPutRequest.setID(id); + projectPutRequest.setOrganizationID(organizationId); + projectPutRequest.setName(name); + projectsCommand.setUpdate(projectPutRequest); + command.setProjects(projectsCommand); + + ResponseForProjectResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForProjectResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Project update failed"); + } + + return response.getData(); + } + + public ProjectsDeleteResponse delete(UUID[] ids) { + Command command = new Command(); + ProjectsCommand projectsCommand = new ProjectsCommand(); + ProjectsDeleteRequest projectsDeleteRequest = new ProjectsDeleteRequest(); + projectsDeleteRequest.setIDS(ids); + projectsCommand.setDelete(projectsDeleteRequest); + command.setProjects(projectsCommand); + + ResponseForProjectsDeleteResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForProjectsDeleteResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? + response.getErrorMessage() : "Projects update failed"); + } + + return response.getData(); + } + + public ProjectsResponse list(UUID organizationId) { + Command command = new Command(); + ProjectsCommand projectsCommand = new ProjectsCommand(); + ProjectsListRequest projectsListRequest = new ProjectsListRequest(); + projectsListRequest.setOrganizationID(organizationId); + projectsCommand.setList(projectsListRequest); + command.setProjects(projectsCommand); + + ResponseForProjectsResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForProjectsResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? + response.getErrorMessage() : "No projects for given organization"); + } + + return response.getData(); + } +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/SecretsClient.java b/languages/java/src/main/java/com/bitwarden/sdk/SecretsClient.java new file mode 100644 index 000000000..f1a97fdc7 --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/SecretsClient.java @@ -0,0 +1,115 @@ +package com.bitwarden.sdk; + +import com.bitwarden.sdk.schema.*; + +import java.util.UUID; + +public class SecretsClient { + + private final CommandRunner commandRunner; + + SecretsClient(CommandRunner commandRunner) { + this.commandRunner = commandRunner; + } + + public SecretResponse get(UUID id) { + Command command = new Command(); + SecretsCommand secretsCommand = new SecretsCommand(); + SecretGetRequest secretGetRequest = new SecretGetRequest(); + secretGetRequest.setID(id); + secretsCommand.setGet(secretGetRequest); + command.setSecrets(secretsCommand); + + ResponseForSecretResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForSecretResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Secret not found"); + } + + return response.getData(); + } + + public SecretResponse create(String key, String value, String note, UUID organizationId, UUID[] projectIds) { + Command command = new Command(); + SecretsCommand secretsCommand = new SecretsCommand(); + SecretCreateRequest secretCreateRequest = new SecretCreateRequest(); + secretCreateRequest.setKey(key); + secretCreateRequest.setValue(value); + secretCreateRequest.setNote(note); + secretCreateRequest.setOrganizationID(organizationId); + secretCreateRequest.setProjectIDS(projectIds); + secretsCommand.setCreate(secretCreateRequest); + command.setSecrets(secretsCommand); + + ResponseForSecretResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForSecretResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Secret create failed"); + } + + return response.getData(); + } + + public SecretResponse update(UUID id, String key, String value, String note, UUID organizationId, + UUID[] projectIds) { + Command command = new Command(); + SecretsCommand secretsCommand = new SecretsCommand(); + SecretPutRequest secretPutRequest = new SecretPutRequest(); + secretPutRequest.setID(id); + secretPutRequest.setKey(key); + secretPutRequest.setValue(value); + secretPutRequest.setNote(note); + secretPutRequest.setOrganizationID(organizationId); + secretPutRequest.setProjectIDS(projectIds); + secretsCommand.setUpdate(secretPutRequest); + command.setSecrets(secretsCommand); + + ResponseForSecretResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForSecretResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Secret update failed"); + } + + return response.getData(); + } + + public SecretsDeleteResponse delete(UUID[] ids) { + Command command = new Command(); + SecretsCommand secretsCommand = new SecretsCommand(); + SecretsDeleteRequest secretsDeleteRequest = new SecretsDeleteRequest(); + secretsDeleteRequest.setIDS(ids); + secretsCommand.setDelete(secretsDeleteRequest); + command.setSecrets(secretsCommand); + + ResponseForSecretsDeleteResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForSecretsDeleteResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Secrets delete failed"); + } + + return response.getData(); + } + + public SecretIdentifiersResponse list(UUID organizationId) { + Command command = new Command(); + SecretsCommand secretsCommand = new SecretsCommand(); + SecretIdentifiersRequest secretIdentifiersRequest = new SecretIdentifiersRequest(); + secretIdentifiersRequest.setOrganizationID(organizationId); + secretsCommand.setList(secretIdentifiersRequest); + command.setSecrets(secretsCommand); + + ResponseForSecretIdentifiersResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForSecretIdentifiersResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? + response.getErrorMessage() : "No secrets for given organization"); + } + + return response.getData(); + } +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/ThrowingFunction.java b/languages/java/src/main/java/com/bitwarden/sdk/ThrowingFunction.java new file mode 100644 index 000000000..cab5b1041 --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/ThrowingFunction.java @@ -0,0 +1,7 @@ +package com.bitwarden.sdk; + +@FunctionalInterface +public interface ThrowingFunction { + + R accept(T t) throws E; +} diff --git a/support/scripts/schemas.ts b/support/scripts/schemas.ts index 148b089e6..7181d6010 100644 --- a/support/scripts/schemas.ts +++ b/support/scripts/schemas.ts @@ -1,4 +1,10 @@ -import { quicktype, InputData, JSONSchemaInput, FetchingJSONSchemaStore } from "quicktype-core"; +import { + quicktype, + quicktypeMultiFile, + InputData, + JSONSchemaInput, + FetchingJSONSchemaStore, +} from "quicktype-core"; import fs from "fs"; import path from "path"; @@ -63,6 +69,23 @@ async function main() { }); writeToFile("./languages/csharp/Bitwarden.Sdk/schemas.cs", csharp.lines); + + const java = await quicktypeMultiFile({ + inputData, + lang: "java", + rendererOptions: { + package: "com.bitwarden.sdk.schema", + "java-version": "8", + }, + }); + + const javaDir = "./languages/java/src/main/java/com/bitwarden/sdk/schema/"; + if (!fs.existsSync(javaDir)) { + fs.mkdirSync(javaDir); + } + java.forEach((file, path) => { + writeToFile(javaDir + path, file.lines); + }); } main();