From 683737007ef53e3276bb8916ab54049f6a8fd735 Mon Sep 17 00:00:00 2001 From: Dillon Giacoppo Date: Wed, 11 Apr 2018 13:11:11 +1000 Subject: [PATCH] Release v1.0.0 --- .travis.yml | 53 ++ CODE_OF_CONDUCT.md | 27 + CONTRIBUTING.md | 58 ++ Gopkg.lock | 33 + Gopkg.toml | 30 + LICENSE | 13 + Makefile | 11 + README.md | 156 ++++ cmd/examples/basicauth/main.go | 28 + cmd/examples/simple/main.go | 39 + cmd/examples/tokenauth/main.go | 27 + misc/hooks/pre-commit | 11 + misc/scripts/deps-ensure | 5 + misc/scripts/install-hooks | 15 + pkg/artifactory/artifactory.go | 386 ++++++++++ pkg/artifactory/artifactory_test.go | 54 ++ pkg/artifactory/artifacts.go | 111 +++ pkg/artifactory/repositories.go | 396 ++++++++++ pkg/artifactory/repositories_test.go | 1 + pkg/artifactory/security.go | 1065 ++++++++++++++++++++++++++ pkg/artifactory/security_test.go | 1 + pkg/artifactory/system.go | 328 ++++++++ pkg/artifactory/system_test.go | 1 + 23 files changed, 2849 insertions(+) create mode 100644 .travis.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Gopkg.lock create mode 100644 Gopkg.toml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/examples/basicauth/main.go create mode 100644 cmd/examples/simple/main.go create mode 100644 cmd/examples/tokenauth/main.go create mode 100644 misc/hooks/pre-commit create mode 100755 misc/scripts/deps-ensure create mode 100755 misc/scripts/install-hooks create mode 100644 pkg/artifactory/artifactory.go create mode 100644 pkg/artifactory/artifactory_test.go create mode 100644 pkg/artifactory/artifacts.go create mode 100644 pkg/artifactory/repositories.go create mode 100644 pkg/artifactory/repositories_test.go create mode 100644 pkg/artifactory/security.go create mode 100644 pkg/artifactory/security_test.go create mode 100644 pkg/artifactory/system.go create mode 100644 pkg/artifactory/system_test.go diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1c7d0ae --- /dev/null +++ b/.travis.yml @@ -0,0 +1,53 @@ +# Adapted from https://gist.github.com/y0ssar1an/df2dab474520c4086926f672c52db139 +language: go + +# Only the last two Go releases are supported by the Go team with security +# updates. Any versions older than that should be considered deprecated. +# Don't bother testing with them. tip builds your code with the latest +# development version of Go. This can warn you that your code will break +# in the next version of Go. Don't worry! Later we declare that test runs +# are allowed to fail on Go tip. +go: + - 1.10 + - master + +matrix: + # It's ok if our code fails on unstable development versions of Go. + allow_failures: + - go: master + # Don't wait for tip tests to finish. Mark the test run green if the + # tests pass on the stable versions of Go. + fast_finish: true + +# Don't email me the results of the test runs. +notifications: + email: false + +env: + - DEP_VERSION="0.4.1" + +before_install: + # Download the binary to bin folder in $GOPATH + - curl -L -s https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -o $GOPATH/bin/dep + # Make the binary executable + - chmod +x $GOPATH/bin/dep + +install: + - dep ensure + +# Anything in before_script that returns a nonzero exit code will +# flunk the build and immediately stop. It's sorta like having +# set -e enabled in bash. +before_script: + - GO_FILES=( $(find . -iname '*.go' -type f | grep -v /vendor/) ) # All the .go files, excluding vendor/ + - go get honnef.co/go/tools/cmd/megacheck # Badass static analyzer/linter + - go get github.com/fzipp/gocyclo + +# script always run to completion (set +e). All of these code checks are must haves +# in a modern Go project. +script: + - test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt + - go test -v -race ./... # Run all the tests with the race detector enabled + - go vet ./... # go vet is the official Go static analyzer + - megacheck ./... # "go vet on steroids" + linter + - gocyclo -over 19 $GO_FILES # forbid code with huge functions diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..1c8292d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,27 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic addresses, without explicit permission +* Submitting contributions or comments that you know to violate the intellectual property or privacy rights of others +* Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. +By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer. Complaints will result in a response and be reviewed and investigated in a way that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.3.0, available at [http://contributor-covenant.org/version/1/3/0/][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/3/0/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7033f46 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,58 @@ +# Contributing to go-artifactory + +## Reporting Issues + +This section guides you through submitting a bug report for go-artifactory. Following these guidelines helps us and the community understand your issue, reproduce the behavior, and find related issues. + +When you are creating an issue, please include as many details as possible. + +### Before submitting an issue + +* **Perform a [cursory search][IssueTracker]** to see if the problem has already been reported. If it has, add a comment to the existing issue instead of opening a new one. + +### How do I submit a (good) issue? + +* **Use a clear and descriptive title** for the issue to identify the problem. +* **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did, but explain how you did it**. For example, if you opened a inline dialog, explain if you used the mouse, or a keyboard shortcut. +* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below. + +Include details about your configuration and environment: + +* **Which OS are you running on?** +* **What version of golang are you using**? + +### Code Contributions + +#### Why should I contribute? + +1. While we strive to look at new issues as soon as we can, because of the many priorities we juggle and limited resources, issues raised often don't get looked into soon enough. +2. We want your contributions. We are always trying to improve our docs, processes and tools to make it easier to submit your own changes. +3. At Atlassian, "Play, As A Team" is one of our values. We encourage cross team contributions and collaborations. + +Please raise a new issue [here][IssueTracker]. + +### Follow code style guidelines + +It is recommended you use the git hooks found in the misc directory, this will include go-fmt + +## Merge into master +All new feature code must be completed in a feature branch and have a corresponding Feature or Bug issue in the go-artifactory project. + +Once you are happy with your changes, you must push your branch to Bitbucket and create a pull request. All pull requests must have at least 2 reviewers from the go-artifactory team. Once the pull request has been approved it may be merged into develop. + +A separate pull request can be made to create a release and merge develop into master. + +Each PR should consist of exactly one commit, use git rebase and squash, and should be as small as possible. If you feel multiple commits are warrented you should probably be filing them as multiple PRs. + +**Attention!**: *Merging into master will automatically release a component. See below for more details* + +## Release a component +Releasing components is completely automated. The process of releasing will begin when changes are made to the `master` branch: + +* Pipelines will move the go branch forward after successful build on master. This will change the version acquired by go-get + +## Root dependencies + +go-artifactory endeavours to avoid external dependencies and be lightweight. + +[IssueTracker]: https://github.com/atlassian/go-artifactory/issues \ No newline at end of file diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..3d97cdf --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,33 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/davecgh/go-spew" + packages = ["spew"] + revision = "346938d642f2ec3594ed81d874461961cd0faa76" + version = "v1.1.0" + +[[projects]] + branch = "master" + name = "github.com/google/go-querystring" + packages = ["query"] + revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a" + +[[projects]] + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" + +[[projects]] + name = "github.com/stretchr/testify" + packages = ["assert"] + revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" + version = "v1.2.1" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "c98be6c3526f7262716cbaeadae88a4da9a8eff226727f09f6518e3309751e76" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..a6e1c32 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,30 @@ + +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" + + +[[constraint]] + branch = "master" + name = "github.com/google/go-querystring" + +[[constraint]] + name = "github.com/stretchr/testify" + version = "1.2.1" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3e8d8e6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright @ 2018 Atlassian Pty Ltd + +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 + + http://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. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ed01dfc --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +all: install-hooks test + +install-hooks: + @misc/scripts/install-hooks + +dep: + @misc/scripts/deps-ensure + @dep ensure -v + +test: + @go test -v ./pkg/... diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f42747 --- /dev/null +++ b/README.md @@ -0,0 +1,156 @@ +# go-artifactory # +go-artifactory is a Go client library for accessing the [Artifactory API](https://www.jfrog.com/confluence/display/RTF/Artifactory+REST+API) + +go-artifactory is tested on Go version 1.9 + +## Usage ## +```go +import "github.com/atlassian/go-artifactory/pkg/artifactory" +``` + +Construct a new Artifactory client, then use the various services on the client to +access different parts of the Artifactory API. For example: + +```go +client := artifactory.NewClient("http://localhost/artifactory", nil) + +// list all repositories +repos, resp, err := client.Repositories.List(context.Background(), nil) +``` + +Some API methods have optional parameters that can be passed. For example: + +```go +client := artifactroy.NewClient("http://localhost/artifactory", nil) + +// list all public local repositories +opt := &artifactory.RepositoryListOptions{Type: "local"} +client.Repositories.ListRepositories(ctx, opt) +``` + +The services of a client divide the API into logical chunks and correspond to +the structure of the Artifactory API documentation at +[https://www.jfrog.com/confluence/display/RTF/Artifactory+REST+API](https://www.jfrog.com/confluence/display/RTF/Artifactory+REST+API). + +NOTE: Using the [context](https://godoc.org/context) package, one can easily +pass cancelation signals and deadlines to various services of the client for +handling a request. In case there is no context available, then `context.Background()` +can be used as a starting point. + +### Authentication ### + +The go-artifactory library does not directly handle authentication. Instead, when +creating a new client, pass an `http.Client` that can handle authentication for +you. + +For API methods that require HTTP Basic Authentication, use the BasicAuthTransport or TokenTransport + +```go +package main + +import ( + "github.com/atlassian/go-artifactory/pkg/artifactory" + "fmt" + "context" +) + +func main() { + tp := artifactory.BasicAuthTransport{ + Username: "", + Password: "", + } + + client, err := artifactory.NewClient("https://localhost/artifactory", tp.Client()) + if err != nil { + fmt.Println(err.Error()) + } + + repos, resp, err := client.Repositories.ListRepositories(context.Background(), nil) +} +``` + +### Creating and Updating Resources ### +All structs for GitHub resources use pointer values for all non-repeated fields. +This allows distinguishing between unset fields and those set to a zero-value. +Helper functions have been provided to easily create these pointers for string, +bool, and int values. For example: + +```go + // create a new local repository named "lib-releases" + repo := artifactory.LocalRepository{ + Key: artifactory.String("lib-releases"), + RClass: artifactory.String("local"), + PackageType: artifactory.String("maven"), + HandleSnapshots: artifactory.Bool(false); + } + + client.Repositories.CreateLocal(context.Background(), &repo) +``` + +Users who have worked with protocol buffers should find this pattern familiar. + +## Roadmap ## + +This library is being initially developed for an internal application at +Atlassian, so API methods will likely be implemented in the order that they are +needed by that application. Eventually, it would be ideal to cover the entire +Artifactory API, so contributions are of course always welcome. The +calling pattern is pretty well established, so adding new methods is relatively +straightforward. + +## Versioning ## + +In general, go-artifactory follows [semver](https://semver.org/) as closely as we +can for tagging releases of the package. For self-contained libraries, the +application of semantic versioning is relatively straightforward and generally +understood. But because go-artifactory is a client library for the Artifactory API +we've adopted the following versioning policy: + +* We increment the **major version** with any incompatible change to + functionality, including changes to the exported Go API surface + or behavior of the API. +* We increment the **minor version** with any backwards-compatible changes to + functionality. +* We increment the **patch version** with any backwards-compatible bug fixes. + +Generally methods will be annotated with a since version. + +## Reporting issues ## + +We believe in open contributions and the power of a strong development community. Please read our [Contributing guidelines][CONTRIBUTING] on how to contribute back and report issues to go-stride. + +## Contributors ## + +Pull requests, issues and comments are welcomed. For pull requests: + +* Add tests for new features and bug fixes +* Follow the existing style +* Separate unrelated changes into multiple pull requests +* Read [Contributing guidelines][CONTRIBUTING] for more details + +See the existing issues for things to start contributing. + +For bigger changes, make sure you start a discussion first by creating +an issue and explaining the intended change. + +Atlassian requires contributors to sign a Contributor License Agreement, +known as a CLA. This serves as a record stating that the contributor is +entitled to contribute the code/documentation/translation to the project +and is willing to have it used in distributions and derivative works +(or is willing to transfer ownership). + +Prior to accepting your contributions we ask that you please follow the appropriate +link below to digitally sign the CLA. The Corporate CLA is for those who are +contributing as a member of an organization and the individual CLA is for +those contributing as an individual. + +* [CLA for corporate contributors](https://na2.docusign.net/Member/PowerFormSigning.aspx?PowerFormId=e1c17c66-ca4d-4aab-a953-2c231af4a20b) +* [CLA for individuals](https://na2.docusign.net/Member/PowerFormSigning.aspx?PowerFormId=3f94fbdc-2fbe-46ac-b14c-5d152700ae5d) + + +## License ## +Copyright (c) 2017 Atlassian and others. Apache 2.0 licensed, see [LICENSE][LICENSE] file. + + +[CONTRIBUTING]: ./CONTRIBUTING.md +[LICENSE]: ./LICENSE.txt \ No newline at end of file diff --git a/cmd/examples/basicauth/main.go b/cmd/examples/basicauth/main.go new file mode 100644 index 0000000..70c08c3 --- /dev/null +++ b/cmd/examples/basicauth/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "fmt" + "github.com/atlassian/go-artifactory/pkg/artifactory" + "os" +) + +func main() { + tp := artifactory.BasicAuthTransport{ + Username: os.Getenv("ARTIFACTORY_USERNAME"), + Password: os.Getenv("ARTIFACTORY_PASSWORD"), + } + + client, err := artifactory.NewClient(os.Getenv("ARTIFACTORY_URL"), tp.Client()) + if err != nil { + fmt.Printf("\nerror: %v\n", err) + return + } + + _, _, err = client.System.Ping(context.Background()) + if err != nil { + fmt.Printf("\nerror: %v\n", err) + } else { + fmt.Println("OK") + } +} diff --git a/cmd/examples/simple/main.go b/cmd/examples/simple/main.go new file mode 100644 index 0000000..f59972d --- /dev/null +++ b/cmd/examples/simple/main.go @@ -0,0 +1,39 @@ +// Retrieves list of all repositories for an artifactory instance +package main + +import ( + "context" + "fmt" + "github.com/atlassian/go-artifactory/pkg/artifactory" + "os" +) + +func main() { + tp := artifactory.BasicAuthTransport{ + Username: os.Getenv("ARTIFACTORY_USERNAME"), + Password: os.Getenv("ARTIFACTORY_PASSWORD"), + } + + client, err := artifactory.NewClient(os.Getenv("ARTIFACTORY_URL"), tp.Client()) + if err != nil { + fmt.Printf("\nerror: %v\n", err) + return + } + + opts := artifactory.RepositoryListOptions{ + Type: "local", + } + repos, _, err := client.Repositories.ListRepositories(context.Background(), &opts) + if err != nil { + fmt.Printf("\nerror: %v\n", err) + return + } else if repos == nil { + fmt.Printf("\nerror: repos cannot be nil\n") + return + } + + fmt.Println("Found these local repos:") + for _, repo := range *repos { + fmt.Println(repo.Key) + } +} diff --git a/cmd/examples/tokenauth/main.go b/cmd/examples/tokenauth/main.go new file mode 100644 index 0000000..f412a91 --- /dev/null +++ b/cmd/examples/tokenauth/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" + "github.com/atlassian/go-artifactory/pkg/artifactory" + "os" +) + +func main() { + tp := artifactory.TokenAuthTransport{ + Token: os.Getenv("ARTIFACTORY_TOKEN"), + } + + client, err := artifactory.NewClient(os.Getenv("ARTIFACTORY_URL"), tp.Client()) + if err != nil { + fmt.Printf("\nerror: %v\n", err) + return + } + + _, _, err = client.System.Ping(context.Background()) + if err != nil { + fmt.Printf("\nerror: %v\n", err) + } else { + fmt.Println("OK") + } +} diff --git a/misc/hooks/pre-commit b/misc/hooks/pre-commit new file mode 100644 index 0000000..cc7d08b --- /dev/null +++ b/misc/hooks/pre-commit @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +go fmt ./pkg/... + +# re-add formatted files +for file in `git diff --cached --name-only` +do + git add $file +done + +go test -v ./pkg/... \ No newline at end of file diff --git a/misc/scripts/deps-ensure b/misc/scripts/deps-ensure new file mode 100755 index 0000000..d194df9 --- /dev/null +++ b/misc/scripts/deps-ensure @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +if [[ ! -e $GOPATH/bin/dep ]]; then + go get -v -u github.com/golang/dep/cmd/dep +fi \ No newline at end of file diff --git a/misc/scripts/install-hooks b/misc/scripts/install-hooks new file mode 100755 index 0000000..108f62a --- /dev/null +++ b/misc/scripts/install-hooks @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +GIT_HOOKS=".git/hooks" +MISC_HOOKS="misc/hooks" + +for file in $(ls $MISC_HOOKS/ | grep -v \\.) +do + +cat > $GIT_HOOKS/$file << EOF +#! /bin/bash +$MISC_HOOKS/$file +exit $? +EOF + +done \ No newline at end of file diff --git a/pkg/artifactory/artifactory.go b/pkg/artifactory/artifactory.go new file mode 100644 index 0000000..8dd6246 --- /dev/null +++ b/pkg/artifactory/artifactory.go @@ -0,0 +1,386 @@ +package artifactory + +import ( + "bytes" + "context" + "crypto/sha1" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "reflect" + "strings" + + "github.com/google/go-querystring/query" + "path" +) + +const ( + userAgent = "go-artifactory" + + headerChecksumSha1 = "X-Checksum-Sha1" + headerResultDetail = "X-Result-Detail" + headerApiToken = "X-JFrog-Art-Api" + + mediaTypePlain = "text/plain" + mediaTypeJson = "application/json" + mediaTypeXml = "application/xml" + mediaTypeLocalRepository = "application/vnd.org.jfrog.artifactory.repositories.LocalRepositoryConfiguration+json" + mediaTypeRemoteRepository = "application/vnd.org.jfrog.artifactory.repositories.RemoteRepositoryConfiguration+json" + mediaTypeVirtualRepository = "application/vnd.org.jfrog.artifactory.repositories.VirtualRepositoryConfiguration+json" + mediaTypeRepositoryDetails = "application/vnd.org.jfrog.artifactory.repositories.RepositoryDetailsList+json" + mediaTypeSystemVersion = "application/vnd.org.jfrog.artifactory.system.Version+json" + mediaTypeUsers = "application/vnd.org.jfrog.artifactory.security.Users+json" + mediaTypeUser = "application/vnd.org.jfrog.artifactory.security.User+json" + mediaTypeGroups = "application/vnd.org.jfrog.artifactory.security.Groups+json" + mediaTypeGroup = "application/vnd.org.jfrog.artifactory.security.Group+json" + mediaTypePermissionTargets = "application/vnd.org.jfrog.artifactory.security.PermissionTargets+json" + mediaTypePermissionTarget = "application/vnd.org.jfrog.artifactory.security.PermissionTarget+json" + mediaTypeItemPermissions = "application/vnd.org.jfrog.artifactory.storage.ItemPermissions+json" + mediaTypeForm = "application/x-www-form-urlencoded" + mediaTypeReplicationConfig = "application/vnd.org.jfrog.artifactory.replications.ReplicationConfigRequest+json" +) + +// Client is the container for all the api methods +type Client struct { + client *http.Client // HTTP client used to communicate with the API. + + // Base URL for API requests. BaseURL should always be specified with a trailing slash. + BaseURL *url.URL + + // User agent used when communicating with the Artifactory API. + UserAgent string + + common service // Reuse a single struct instead of allocating one for each service on the heap. + + // Services used for talking to different parts of the Artifactory API. + Repositories *RepositoriesService + Security *SecurityService + System *SystemService + Artifacts *ArtifactService +} + +type service struct { + client *Client +} + +// NewClient creates a Client from a provided base url for an artifactory instance and a http client +func NewClient(baseURL string, httpClient *http.Client) (*Client, error) { + if httpClient == nil { + httpClient = http.DefaultClient + } + + baseEndpoint, err := url.Parse(baseURL) + + if err != nil { + return nil, err + } + + if !strings.HasSuffix(baseEndpoint.Path, "/") { + baseEndpoint.Path += "/" + } + + c := &Client{client: httpClient, BaseURL: baseEndpoint, UserAgent: userAgent} + c.common.client = c + + c.Repositories = (*RepositoriesService)(&c.common) + c.Security = (*SecurityService)(&c.common) + c.System = (*SystemService)(&c.common) + c.Artifacts = (*ArtifactService)(&c.common) + return c, nil +} + +// NewRequest creates an API request. A relative URL can be provided in urlStr, in which case it is resolved relative to the BaseURL +// of the Client. Relative URLs should always be specified without a preceding slash. If specified, the value pointed to +// by body is included as the request body. +func (c *Client) NewRequest(method, urlStr string, body io.Reader) (*http.Request, error) { + u, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, urlStr)) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(method, u.String(), body) + if err != nil { + return nil, err + } + + if c.UserAgent != "" { + req.Header.Set("User-Agent", c.UserAgent) + } + return req, nil +} + +// NewJSONEncodedRequest is a wrapper around client.NewRequest which encodes the body as a JSON object +func (c *Client) NewJSONEncodedRequest(method, urlStr string, body interface{}) (*http.Request, error) { + var buf io.ReadWriter + if body != nil { + buf = new(bytes.Buffer) + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + err := enc.Encode(body) + if err != nil { + return nil, err + } + } + + req, err := c.NewRequest(method, urlStr, buf) + if err != nil { + return nil, err + } + if body != nil { + req.Header.Set("Content-Type", mediaTypeJson) + } + return req, nil +} + +// NewURLEncodedRequest is a wrapper around client.NewRequest which encodes the body with URL encoding +func (c *Client) NewURLEncodedRequest(method, urlStr string, body interface{}) (*http.Request, error) { + var buf io.Reader + if body != nil { + urlVals, err := query.Values(body) + if err != nil { + return nil, err + } + buf = strings.NewReader(urlVals.Encode()) + } + + req, err := c.NewRequest(method, urlStr, buf) + if err != nil { + return nil, err + } + if body != nil { + req.Header.Set("Content-Type", mediaTypeForm) + } + return req, nil +} + +// Do executes a give request with the given context. If the parameter v is a writer the body will be written to it in +// raw format, else v is assumed to be a struct to unmarshal the body into assuming JSON format. If v is nil then the +// body is not read and can be manually parsed from the response +func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) { + req = req.WithContext(ctx) + resp, err := c.client.Do(req) + if err != nil { + // If we got an error, and the context has been canceled, + // the context's error is probably more useful. + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + if e, ok := err.(*url.Error); ok { + if url2, err := url.Parse(e.URL); err == nil { + e.URL = url2.String() + return nil, e + } + } + + return nil, err + } + defer resp.Body.Close() + + err = checkResponse(resp) + if err != nil { + // even though there was an error, we still return the response + // in case the caller wants to inspect it further + return resp, err + } + + if v != nil { + if w, ok := v.(io.Writer); ok { + io.Copy(w, resp.Body) + } else { + err = json.NewDecoder(resp.Body).Decode(v) + if err == io.EOF { + err = nil // ignore EOF errors caused by empty response body + } + } + } + + return resp, err +} + +func addOptions(s string, opt interface{}) (string, error) { + v := reflect.ValueOf(opt) + if v.Kind() == reflect.Ptr && v.IsNil() { + return s, nil + } + + u, err := url.Parse(s) + if err != nil { + return s, err + } + + qs, err := query.Values(opt) + if err != nil { + return s, err + } + + u.RawQuery = qs.Encode() + return u.String(), nil +} + +// CheckResponse checks the API response for errors, and returns them if present. A response is considered an error if +// it has a status code outside the 200 range. If parsing the response leads to an empty error object, the response will +// be returned as plain text +func checkResponse(r *http.Response) error { + if c := r.StatusCode; 200 <= c && c <= 299 { + return nil + } + errorResponse := &ErrorResponse{Response: r} + data, err := ioutil.ReadAll(r.Body) + if err == nil && data != nil { + err = json.Unmarshal(data, errorResponse) + if err != nil || len(errorResponse.Errors) == 0 { + return fmt.Errorf(string(data)) + } + } + + return errorResponse +} + +// ErrorResponse reports one or more errors caused by an API request. +type ErrorResponse struct { + Response *http.Response `json:"-"` // HTTP response that caused this error + Errors []Status `json:"errors,omitempty"` // Individual errors +} + +// Status is the individual error provided by the API +type Status struct { + Status int `json:"status"` // Validation error status code + Message string `json:"message"` // Message describing the error. Errors with Code == "custom" will always have this set. +} + +func (e *Status) Error() string { + return fmt.Sprintf("%d error caused by %s", e.Status, e.Message) +} + +func (r *ErrorResponse) Error() string { + return fmt.Sprintf("%v %v: %d %+v", r.Response.Request.Method, r.Response.Request.URL, + r.Response.StatusCode, r.Errors) +} + +// ClientProvider exposes a Client method and enforces a standard for the custom transoirts +type ClientProvider interface { + Client() *http.Client +} + +// BasicAuthTransport allows the construction of a HTTP client that authenticates with basic auth +// It also adds the correct headers to the request +type BasicAuthTransport struct { + Username string + Password string + Transport http.RoundTripper +} + +// Client returns a HTTP client and injects the basic auth transport +func (t *BasicAuthTransport) Client() *http.Client { + return &http.Client{Transport: t} +} + +func (t *BasicAuthTransport) transport() http.RoundTripper { + if t.Transport != nil { + return t.Transport + } + return http.DefaultTransport +} + +// RoundTrip allows us to add headers to every request +func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // To set extra headers, we must make a copy of the Request so + // that we don't modify the Request we were given. This is required by the + // specification of http.RoundTripper. + // + // Since we are going to modify only req.Header here, we only need a deep copy + // of req.Header. + req2 := new(http.Request) + deepCopyRequest(req, req2) + + req2.SetBasicAuth(t.Username, t.Password) + req2.Header.Add(headerResultDetail, "info, properties") + + if req.Body != nil { + reader, _ := req.GetBody() + buf, _ := ioutil.ReadAll(reader) + chkSum := getSha1(buf) + req.Header.Add(headerChecksumSha1, fmt.Sprintf("%x", chkSum)) + } + + return t.transport().RoundTrip(req2) +} + +// TokenAuthTransport exposes a HTTP client which uses this transport. It authenticates via an Artifactory API token +// It also adds the correct headers to the request +type TokenAuthTransport struct { + Token string + Transport http.RoundTripper +} + +// Client returns a HTTP client and injects the token auth transport +func (t *TokenAuthTransport) Client() *http.Client { + return &http.Client{Transport: t} +} + +func (t *TokenAuthTransport) transport() http.RoundTripper { + if t.Transport != nil { + return t.Transport + } + return http.DefaultTransport +} + +// RoundTrip allows us to add headers to every request +func (t *TokenAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // To set extra headers, we must make a copy of the Request so + // that we don't modify the Request we were given. This is required by the + // specification of http.RoundTripper. + // + // Since we are going to modify only req.Header here, we only need a deep copy + // of req.Header. + req2 := new(http.Request) + deepCopyRequest(req, req2) + + req2.Header.Set(headerApiToken, t.Token) + req2.Header.Add(headerResultDetail, "info, properties") + + if req.Body != nil { + reader, _ := req.GetBody() + buf, _ := ioutil.ReadAll(reader) + chkSum := getSha1(buf) + req.Header.Add(headerChecksumSha1, fmt.Sprintf("%x", chkSum)) + } + + return t.transport().RoundTrip(req2) +} + +func getSha1(buf []byte) []byte { + h := sha1.New() + h.Write(buf) + return h.Sum(nil) +} + +func deepCopyRequest(req *http.Request, req2 *http.Request) { + *req2 = *req + req2.Header = make(http.Header, len(req.Header)) + for k, s := range req.Header { + req2.Header[k] = append([]string(nil), s...) + } +} + +// Bool is a helper routine that allocates a new bool value +// to store v and returns a pointer to it. +func Bool(v bool) *bool { return &v } + +// Int is a helper routine that allocates a new int value +// to store v and returns a pointer to it. +func Int(v int) *int { return &v } + +// Int64 is a helper routine that allocates a new int64 value +// to store v and returns a pointer to it. +func Int64(v int64) *int64 { return &v } + +// String is a helper routine that allocates a new string value +// to store v and returns a pointer to it. +func String(v string) *string { return &v } diff --git a/pkg/artifactory/artifactory_test.go b/pkg/artifactory/artifactory_test.go new file mode 100644 index 0000000..3c742a2 --- /dev/null +++ b/pkg/artifactory/artifactory_test.go @@ -0,0 +1,54 @@ +package artifactory + +import ( + "context" + "fmt" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +func TestBasicAuthTransport(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, pass, _ := r.BasicAuth() + assert.Equal(t, "username", user) + assert.Equal(t, "password", pass) + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, "pong") + })) + + tp := BasicAuthTransport{ + Username: "username", + Password: "password", + } + + client, err := NewClient(server.URL, tp.Client()) + assert.Nil(t, err) + + _, _, err = client.System.Ping(context.Background()) + assert.Nil(t, err) +} + +func TestTokenAuthTransport(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-JFrog-Art-Api") + assert.Equal(t, "token", token) + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, "pong") + })) + + tp := TokenAuthTransport{ + Token: "token", + } + + client, err := NewClient(server.URL, tp.Client()) + assert.Nil(t, err) + + _, _, err = client.System.Ping(context.Background()) + assert.Nil(t, err) +} diff --git a/pkg/artifactory/artifacts.go b/pkg/artifactory/artifacts.go new file mode 100644 index 0000000..5588da9 --- /dev/null +++ b/pkg/artifactory/artifacts.go @@ -0,0 +1,111 @@ +package artifactory + +import ( + "context" + "encoding/json" + "fmt" + "net/http" +) + +// ArtifactService exposes the Artifact API endpoints from Artifactory +type ArtifactService service + +// SingleReplicationConfig is the model of the Artifactory Replication Config +type SingleReplicationConfig struct { + RepoKey *string `json:"repoKey,omitempty"` + URL *string `json:"url,omitempty"` + SocketTimeoutMillis *int `json:"socketTimeoutMillis,omitempty"` + Username *string `json:"username,omitempty"` + Password *string `json:"password,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + SyncDeletes *bool `json:"syncDeletes,omitempty"` + SyncProperties *bool `json:"syncProperties,omitempty"` + SyncStatistics *bool `json:"syncStatistics,omitempty"` + PathPrefix *string `json:"pathPrefix,omitempty"` + CronExp *string `json:"cronExp,omitempty"` // Only required when getting list of repositories as C*UD operations will be done through a repConfig obj + EnableEventReplication *bool `json:"enableEventReplication,omitempty"` +} + +// ReplicationConfig is the model for the multi replication config API endpoints. Its usage is preferred over +// SingleReplicationConfig as it is a more direct mapping of the replicationConfig in the UI +type ReplicationConfig struct { + RepoKey *string `json:"-"` + CronExp *string `json:"cronExp,omitempty"` + EnableEventReplication *bool `json:"enableEventReplication,omitempty"` + Replications *[]SingleReplicationConfig `json:"replications,omitempty"` +} + +func (r ReplicationConfig) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Add or replace replication configuration for given repository key. Supported by local and remote repositories. Accepts the JSON payload returned from Get Repository Replication Configuration for a single and an array of configurations. If the payload is an array of replication configurations, then values for cronExp and enableEventReplication in the first element in the array will determine the corresponding values when setting the repository replication configuration. +// Notes: Requires Artifactory Pro +// Security: Requires a privileged user +func (s *ArtifactService) SetRepositoryReplicationConfig(ctx context.Context, repoKey string, config *ReplicationConfig) (*http.Response, error) { + path := fmt.Sprintf("/api/replications/multiple/%s", repoKey) + req, err := s.client.NewJSONEncodedRequest("PUT", path, config) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// Returns the replication configuration for the given repository key, if found. Supported by local and remote repositories. Note: The 'enableEventReplication' parameter refers to both push and pull replication. +// Notes: Requires Artifactory Pro +// Security: Requires a privileged user +func (s *ArtifactService) GetRepositoryReplicationConfig(ctx context.Context, repoKey string) (*ReplicationConfig, *http.Response, error) { + path := fmt.Sprintf("/api/replications/%s", repoKey) + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeReplicationConfig) + + replications := make([]SingleReplicationConfig, 0) + resp, err := s.client.Do(ctx, req, &replications) + + if err != nil { + return nil, resp, err + } + + replicationConfig := new(ReplicationConfig) + + if len(replications) > 0 { + replicationConfig.Replications = new([]SingleReplicationConfig) + } + + for _, replication := range replications { + replicationConfig.RepoKey = replication.RepoKey + replicationConfig.CronExp = replication.CronExp + replicationConfig.EnableEventReplication = replication.EnableEventReplication + + *replicationConfig.Replications = append(*replicationConfig.Replications, replication) + } + + return replicationConfig, resp, nil +} + +// Update existing replication configuration for given repository key, if found. Supported by local and remote repositories. +// Notes: Requires Artifactory Pro +// Security: Requires a privileged user +func (s *ArtifactService) UpdateRepositoryReplicationConfig(ctx context.Context, repoKey string, config *ReplicationConfig) (*http.Response, error) { + path := fmt.Sprintf("/api/replications/multiple/%s", repoKey) + req, err := s.client.NewJSONEncodedRequest("POST", path, config) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +func (s *ArtifactService) DeleteRepositoryReplicationConfig(ctx context.Context, repoKey string) (*http.Response, error) { + path := fmt.Sprintf("/api/replications/%s", repoKey) + req, err := s.client.NewJSONEncodedRequest("DELETE", path, nil) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} diff --git a/pkg/artifactory/repositories.go b/pkg/artifactory/repositories.go new file mode 100644 index 0000000..2714c96 --- /dev/null +++ b/pkg/artifactory/repositories.go @@ -0,0 +1,396 @@ +package artifactory + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" +) + +type RepositoriesService service + +type RepositoryDetails struct { + Key *string `json:"key,omitempty"` + Type *string `json:"type,omitempty"` + Description *string `json:"description,omitempty"` + URL *string `json:"url,omitempty"` +} + +func (r RepositoryDetails) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +type RepositoryListOptions struct { + // Type of repositories to list. + // Can be one of local|remote|virtual|distribution. Default: all + Type string `url:"type,omitempty"` +} + +// Returns a list of minimal repository details for all repositories of the specified type. +// Since: 2.2.0 +// Security: Requires a privileged user (can be anonymous) +func (s *RepositoriesService) ListRepositories(ctx context.Context, opt *RepositoryListOptions) (*[]RepositoryDetails, *http.Response, error) { + path := "/api/repositories/" + req, err := s.client.NewJSONEncodedRequest("GET", path, opt) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeRepositoryDetails) + + repos := new([]RepositoryDetails) + resp, err := s.client.Do(ctx, req, &repos) + return repos, resp, err +} + +// application/vnd.org.jfrog.artifactory.repositories.LocalRepositoryConfiguration+json +type LocalRepository struct { + Key *string `json:"key,omitempty"` + RClass *string `json:"rclass,omitempty"` // Mandatory element in create/replace queries (optional in "update" queries) + PackageType *string `json:"packageType,omitempty"` + Description *string `json:"description,omitempty"` + Notes *string `json:"notes,omitempty"` + IncludesPattern *string `json:"includesPattern,omitempty"` + ExcludesPattern *string `json:"excludesPattern,omitempty"` + ArchiveBrowsingEnabled *bool `json:"archiveBrowsingEnabled,omitempty"` + BlackedOut *bool `json:"blackedOut,omitempty"` + BlockXrayUnscannedArtifacts *bool `json:"blockXrayUnscannedArtifacts,omitempty"` + CalculateYumMetadata *bool `json:"calculateYumMetadata,omitempty"` + ChecksumPolicyType *string `json:"checksumPolicyType,omitempty"` + DebianTrivialLayout *bool `json:"debianTrivialLayout,omitempty"` + DockerApiVersion *string `json:"dockerApiVersion,omitempty"` + EnableBowerSupport *bool `json:"enableBowerSupport,omitempty"` + EnableCocoaPodsSupport *bool `json:"enableCocoaPodsSupport,omitempty"` + EnableComposerSupport *bool `json:"enableComposerSupport,omitempty"` + EnableConanSupport *bool `json:"enableConanSupport,omitempty"` + EnableDebianSupport *bool `json:"enableDebianSupport,omitempty"` + EnableDistRepoSupport *bool `json:"enableDistRepoSupport,omitempty"` + EnableDockerSupport *bool `json:"enableDockerSupport,omitempty"` + EnableFileListsIndexing *bool `json:"enableFileListsIndexing,omitempty"` + EnableGemsSupport *bool `json:"enableGemsSupport,omitempty"` + EnableGitLfsSupport *bool `json:"enableGitLfsSupport,omitempty"` + EnableNpmSupport *bool `json:"enableNpmSupport,omitempty"` + EnableNuGetSupport *bool `json:"enableNuGetSupport,omitempty"` + EnablePuppetSupport *bool `json:"enablePuppetSupport,omitempty"` + EnablePypiSupport *bool `json:"enablePypiSupport,omitempty"` + EnableVagrantSupport *bool `json:"enableVagrantSupport,omitempty"` + EnabledChefSupport *bool `json:"enabledChefSupport,omitempty"` + ForceNugetAuthentication *bool `json:"forceNugetAuthentication,omitempty"` + HandleReleases *bool `json:"handleReleases,omitempty"` + HandleSnapshots *bool `json:"handleSnapshots,omitempty"` + MaxUniqueSnapshots *int `json:"maxUniqueSnapshots,omitempty"` + MaxUniqueTags *int `json:"maxUniqueTags,omitempty"` + PropertySets *[]string `json:"propertySets,omitempty"` + RepoLayoutRef *string `json:"repoLayoutRef,omitempty"` + SnapshotVersionBehavior *string `json:"snapshotVersionBehavior,omitempty"` + SuppressPomConsistencyChecks *bool `json:"suppressPomConsistencyChecks,omitempty"` + XrayIndex *bool `json:"xrayIndex,omitempty"` + XrayMinimumBlockedSeverity *string `json:"xrayMinimumBlockedSeverity,omitempty"` + YumRootDepth *int `json:"yumRootDepth,omitempty"` +} + +func (r LocalRepository) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Creates a new repository in Artifactory with the provided configuration. +// Since: 2.3.0 +// Notes: Requires Artifactory Pro +// An existing repository with the same key are removed from the configuration and its content is removed! +// Missing values are set to the default values as defined by the consumed type spec. +// Security: Requires an admin user +func (s *RepositoriesService) CreateLocal(ctx context.Context, repo *LocalRepository) (*http.Response, error) { + return s.create(ctx, *repo.Key, repo) +} + +// Retrieves the current configuration of a repository. +// Since: 2.3.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user for complete repository configuration. Non-admin users will receive only partial configuration data. +func (s *RepositoriesService) GetLocal(ctx context.Context, repo string) (*LocalRepository, *http.Response, error) { + repository, resp, err := s.get(ctx, repo, new(LocalRepository)) + if err != nil { + return nil, resp, err + } + return repository.(*LocalRepository), resp, nil +} + +// Updates an exiting repository configuration in Artifactory with the provided configuration elements. +// Since: 2.3.0 +// Notes: Requires Artifactory Pro +// The class of a repository (the rclass attribute cannot be updated. +// Security: Requires an admin user +func (s *RepositoriesService) UpdateLocal(ctx context.Context, repo string, repository *LocalRepository) (*http.Response, error) { + return s.update(ctx, repo, repository) +} + +// Removes a repository configuration together with the whole repository content. +// Since: 2.3.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *RepositoriesService) DeleteLocal(ctx context.Context, repo string) (*http.Response, error) { + return s.delete(ctx, repo) +} + +type Statistics struct { + Enabled *bool `json:"enabled,omitempty"` +} + +type Properties struct { + Enabled *bool `json:"enabled,omitempty"` +} + +type Source struct { + OriginAbsenceDetection *bool `json:"originAbsenceDetection,omitempty"` +} + +type ContentSynchronisation struct { + Enabled *bool `json:"enabled,omitempty"` + Statistics *Statistics `json:"statistics,omitempty"` + Properties *Properties `json:"properties,omitempty"` + Source *Source `json:"source,omitempty"` +} + +type RemoteRepository struct { + Key *string `json:"key,omitempty"` + RClass *string `json:"rclass,omitempty"` // Mandatory element in create/replace queries (optional in "update" queries) + PackageType *string `json:"packageType,omitempty"` + Description *string `json:"description,omitempty"` + Notes *string `json:"notes,omitempty"` + IncludesPattern *string `json:"includesPattern,omitempty"` + ExcludesPattern *string `json:"excludesPattern,omitempty"` + AllowAnyHostAuth *bool `json:"allowAnyHostAuth,omitempty"` + ArchiveBrowsingEnabled *bool `json:"archiveBrowsingEnabled,omitempty"` + AssumedOfflinePeriodSecs *int `json:"assumedOfflinePeriodSecs,omitempty"` + BlackedOut *bool `json:"blackedOut,omitempty"` + BlockMismatchingMimeTypes *bool `json:"blockMismatchingMimeTypes,omitempty"` + BlockXrayUnscannedArtifacts *bool `json:"blockXrayUnscannedArtifacts,omitempty"` + BypassHeadRequests *bool `json:"bypassHeadRequests,omitempty"` + ContentSynchronisation *ContentSynchronisation `json:"contentSynchronisation,omitempty"` + DebianTrivialLayout *bool `json:"debianTrivialLayout,omitempty"` + DockerApiVersion *string `json:"dockerApiVersion,omitempty"` + EnableBowerSupport *bool `json:"enableBowerSupport,omitempty"` + EnableCocoaPodsSupport *bool `json:"enableCocoaPodsSupport,omitempty"` + EnableConanSupport *bool `json:"enableConanSupport,omitempty"` + EnableCookieManagement *bool `json:"enableCookieManagement,omitempty"` + EnabledChefSupport *bool `json:"enabledChefSupport,omitempty"` + EnableComposerSupport *bool `json:"enableComposerSupport,omitempty"` + EnableDebianSupport *bool `json:"enableDebianSupport,omitempty"` + EnableDistRepoSupport *bool `json:"enableDistRepoSupport,omitempty"` + EnableDockerSupport *bool `json:"enableDockerSupport,omitempty"` + EnableGemsSupport *bool `json:"enableGemsSupport,omitempty"` + EnableGitLfsSupport *bool `json:"enableGitLfsSupport,omitempty"` + EnableNpmSupport *bool `json:"enableNpmSupport,omitempty"` + EnableNuGetSupport *bool `json:"enableNuGetSupport,omitempty"` + EnablePuppetSupport *bool `json:"enablePuppetSupport,omitempty"` + EnablePypiSupport *bool `json:"enablePypiSupport,omitempty"` + EnableTokenAuthentication *bool `json:"enableTokenAuthentication,omitempty"` + EnableVagrantSupport *bool `json:"enableVagrantSupport,omitempty"` + FailedRetrievalCachePeriodSecs *int `json:"failedRetrievalCachePeriodSecs,omitempty"` + FetchJarsEagerly *bool `json:"fetchJarsEagerly,omitempty"` + FetchSourcesEagerly *bool `json:"fetchSourcesEagerly,omitempty"` + ForceNugetAuthentication *bool `json:"forceNugetAuthentication,omitempty"` + HandleReleases *bool `json:"handleReleases,omitempty"` + HandleSnapshots *bool `json:"handleSnapshots,omitempty"` + HardFail *bool `json:"hardFail,omitempty"` + ListRemoteFolderItems *bool `json:"listRemoteFolderItems,omitempty"` + LocalAddress *string `json:"localAddress,omitempty"` + MaxUniqueSnapshots *int `json:"maxUniqueSnapshots,omitempty"` + MaxUniqueTags *int `json:"maxUniqueTags,omitempty"` + MismatchingMimeTypesOverrideList *string `json:"mismatchingMimeTypesOverrideList,omitempty"` + MissedRetrievalCachePeriodSecs *int `json:"missedRetrievalCachePeriodSecs,omitempty"` + Offline *bool `json:"offline,omitempty"` + Password *string `json:"password,omitempty"` + PropagateQueryParams *bool `json:"propagateQueryParams,omitempty"` + PropertySets *[]string `json:"propertySets,omitempty"` + Proxy *string `json:"proxy,omitempty"` + RejectInvalidJars *bool `json:"rejectInvalidJars,omitempty"` + RemoteRepoChecksumPolicyType *string `json:"remoteRepoChecksumPolicyType,omitempty"` + RepoLayoutRef *string `json:"repoLayoutRef,omitempty"` + RetrievalCachePeriodSecs *int `json:"retrievalCachePeriodSecs,omitempty"` + ShareConfiguration *bool `json:"shareConfiguration,omitempty"` + SocketTimeoutMillis *int `json:"socketTimeoutMillis,omitempty"` + StoreArtifactsLocally *bool `json:"storeArtifactsLocally,omitempty"` + SuppressPomConsistencyChecks *bool `json:"suppressPomConsistencyChecks,omitempty"` + SynchronizeProperties *bool `json:"synchronizeProperties,omitempty"` + UnusedArtifactsCleanupEnabled *bool `json:"unusedArtifactsCleanupEnabled,omitempty"` + UnusedArtifactsCleanupPeriodHours *int `json:"unusedArtifactsCleanupPeriodHours,omitempty"` + Url *string `json:"url,omitempty"` + Username *string `json:"username,omitempty"` // Mandatory element in create/replace queries (optional in "update" queries) + XrayIndex *bool `json:"xrayIndex,omitempty"` + XrayMinimumBlockedSeverity *string `json:"xrayMinimumBlockedSeverity,omitempty"` + BowerRegistryURL *string `json:"bowerRegistryUrl,omitempty"` + VcsType *string `json:"vcsType,omitempty"` + VcsGitProvider *string `json:"vcsGitProvider,omitempty"` + VcsGitDownloadUrl *string `json:"vcsGitDownloadUrl,omitempty"` + ClientTLSCertificate *string `json:"clientTlsCertificate,omitempty"` +} + +func (r RemoteRepository) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Creates a new repository in Artifactory with the provided configuration. +// Since: 2.3.0 +// Notes: Requires Artifactory Pro +// An existing repository with the same key are removed from the configuration and its content is removed! +// Missing values are set to the default values as defined by the consumed type spec. +// Security: Requires an admin user +func (s *RepositoriesService) CreateRemote(ctx context.Context, repo *RemoteRepository) (*http.Response, error) { + return s.create(ctx, *repo.Key, repo) +} + +// Retrieves the current configuration of a repository. +// Since: 2.3.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user for complete repository configuration. Non-admin users will receive only partial configuration data. +func (s *RepositoriesService) GetRemote(ctx context.Context, repo string) (*RemoteRepository, *http.Response, error) { + repository, resp, err := s.get(ctx, repo, new(RemoteRepository)) + if err != nil { + return nil, resp, err + } + return repository.(*RemoteRepository), resp, nil +} + +// Updates an exiting repository configuration in Artifactory with the provided configuration elements. +// Since: 2.3.0 +// Notes: Requires Artifactory Pro +// The class of a repository (the rclass attribute cannot be updated. +// Security: Requires an admin user +func (s *RepositoriesService) UpdateRemote(ctx context.Context, repo string, repository *RemoteRepository) (*http.Response, error) { + return s.update(ctx, repo, repository) +} + +// Removes a repository configuration together with the whole repository content. +// Since: 2.3.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *RepositoriesService) DeleteRemote(ctx context.Context, repo string) (*http.Response, error) { + return s.delete(ctx, repo) +} + +type VirtualRepository struct { + Key *string `json:"key,omitempty"` + RClass *string `json:"rclass,omitempty"` // Mandatory element in create/replace queries (optional in "update" queries) + PackageType *string `json:"packageType,omitempty"` + Description *string `json:"description,omitempty"` + Notes *string `json:"notes,omitempty"` + IncludesPattern *string `json:"includesPattern,omitempty"` + ExcludesPattern *string `json:"excludesPattern,omitempty"` + ArtifactoryRequestsCanRetrieveRemoteArtifacts *bool `json:"artifactoryRequestsCanRetrieveRemoteArtifacts,omitempty"` + DebianTrivialLayout *bool `json:"debianTrivialLayout,omitempty"` + DefaultDeploymentRepo *string `json:"defaultDeploymentRepo,omitempty"` + DockerApiVersion *string `json:"dockerApiVersion,omitempty"` + EnableBowerSupport *bool `json:"enableBowerSupport,omitempty"` + EnableCocoaPodsSupport *bool `json:"enableCocoaPodsSupport,omitempty"` + EnableConanSupport *bool `json:"enableConanSupport,omitempty"` + EnableComposerSupport *bool `json:"enableComposerSupport,omitempty"` + EnabledChefSupport *bool `json:"enabledChefSupport,omitempty"` + EnableDebianSupport *bool `json:"enableDebianSupport,omitempty"` + EnableDistRepoSupport *bool `json:"enableDistRepoSupport,omitempty"` + EnableDockerSupport *bool `json:"enableDockerSupport,omitempty"` + EnableGemsSupport *bool `json:"enableGemsSupport,omitempty"` + EnableGitLfsSupport *bool `json:"enableGitLfsSupport,omitempty"` + EnableNpmSupport *bool `json:"enableNpmSupport,omitempty"` + EnableNuGetSupport *bool `json:"enableNuGetSupport,omitempty"` + EnablePuppetSupport *bool `json:"enablePuppetSupport,omitempty"` + EnablePypiSupport *bool `json:"enablePypiSupport,omitempty"` + EnableVagrantSupport *bool `json:"enableVagrantSupport,omitempty"` + ExternalDependenciesEnabled *bool `json:"externalDependenciesEnabled,omitempty"` + ForceNugetAuthentication *bool `json:"forceNugetAuthentication,omitempty"` + KeyPair *string `json:"keyPair,omitempty"` + PomRepositoryReferencesCleanupPolicy *string `json:"pomRepositoryReferencesCleanupPolicy,omitempty"` + Repositories *[]string `json:"repositories,omitempty"` + VirtualRetrievalCachePeriodSecs *int `json:"virtualRetrievalCachePeriodSecs,omitempty"` +} + +func (r VirtualRepository) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Creates a new repository in Artifactory with the provided configuration. +// Since: 2.3.0 +// Notes: Requires Artifactory Pro +// An existing repository with the same key are removed from the configuration and its content is removed! +// Missing values are set to the default values as defined by the consumed type spec. +// Security: Requires an admin user +func (s *RepositoriesService) CreateVirtual(ctx context.Context, repo *VirtualRepository) (*http.Response, error) { + return s.create(ctx, *repo.Key, repo) +} + +// Retrieves the current configuration of a repository. +// Since: 2.3.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user for complete repository configuration. Non-admin users will receive only partial configuration data. +func (s *RepositoriesService) GetVirtual(ctx context.Context, repo string) (*VirtualRepository, *http.Response, error) { + repository, resp, err := s.get(ctx, repo, new(VirtualRepository)) + if err != nil { + return nil, resp, err + } + return repository.(*VirtualRepository), resp, nil +} + +// Updates an exiting repository configuration in Artifactory with the provided configuration elements. +// Since: 2.3.0 +// Notes: Requires Artifactory Pro +// The class of a repository (the rclass attribute cannot be updated. +// Security: Requires an admin user +func (s *RepositoriesService) UpdateVirtual(ctx context.Context, repo string, repository *VirtualRepository) (*http.Response, error) { + return s.update(ctx, repo, repository) +} + +// Removes a repository configuration together with the whole repository content. +// Since: 2.3.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *RepositoriesService) DeleteVirtual(ctx context.Context, repo string) (*http.Response, error) { + return s.delete(ctx, repo) +} + +// Generic repo CRUD operations +func (s *RepositoriesService) create(ctx context.Context, repo string, v interface{}) (*http.Response, error) { + path := fmt.Sprintf("/api/repositories/%s", repo) + req, err := s.client.NewJSONEncodedRequest("PUT", path, v) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +func (s *RepositoriesService) get(ctx context.Context, repo string, v interface{}) (interface{}, *http.Response, error) { + path := fmt.Sprintf("/api/repositories/%v", repo) + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + + acceptHeaders := []string{mediaTypeLocalRepository, mediaTypeVirtualRepository, mediaTypeRemoteRepository} + req.Header.Set("Accept", strings.Join(acceptHeaders, ", ")) + + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +func (s *RepositoriesService) update(ctx context.Context, repo string, v interface{}) (*http.Response, error) { + path := fmt.Sprintf("/api/repositories/%v", repo) + req, err := s.client.NewJSONEncodedRequest("POST", path, v) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +func (s *RepositoriesService) delete(ctx context.Context, repo string) (*http.Response, error) { + path := fmt.Sprintf("/api/repositories/%v", repo) + req, err := s.client.NewJSONEncodedRequest("DELETE", path, nil) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} diff --git a/pkg/artifactory/repositories_test.go b/pkg/artifactory/repositories_test.go new file mode 100644 index 0000000..a60755a --- /dev/null +++ b/pkg/artifactory/repositories_test.go @@ -0,0 +1 @@ +package artifactory diff --git a/pkg/artifactory/security.go b/pkg/artifactory/security.go new file mode 100644 index 0000000..efc6728 --- /dev/null +++ b/pkg/artifactory/security.go @@ -0,0 +1,1065 @@ +package artifactory + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "strings" +) + +type SecurityService service + +type UserDetails struct { + Name *string `json:"name,omitempty"` + Uri *string `json:"uri,omitempty"` + Realm *string `json:"realm,omitempty"` +} + +func (r UserDetails) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Get the users list +// Since: 2.4.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) ListUsers(ctx context.Context) (*[]UserDetails, *http.Response, error) { + path := "/api/security/users" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeUsers) + + users := new([]UserDetails) + resp, err := s.client.Do(ctx, req, users) + return users, resp, err +} + +// application/vnd.org.jfrog.artifactory.security.User+json +type User struct { + Name *string `json:"name,omitempty"` // Optional element in create/replace queries + Email *string `json:"email,omitempty"` // Mandatory element in create/replace queries, optional in "update" queries + Password *string `json:"password,omitempty"` // Mandatory element in create/replace queries, optional in "update" queries + Admin *bool `json:"admin,omitempty"` // Optional element in create/replace queries; Default: false + ProfileUpdatable *bool `json:"profileUpdatable,omitempty"` // Optional element in create/replace queries; Default: true + DisableUIAccess *bool `json:"disableUIAccess,omitempty"` // Optional element in create/replace queries; Default: false + InternalPasswordDisabled *bool `json:"internalPasswordDisabled,omitempty"` // Optional element in create/replace queries; Default: false + LastLoggedIn *string `json:"lastLoggedIn,omitempty"` // Read-only element + Realm *string `json:"realm,omitempty"` // Read-only element + Groups *[]string `json:"groups,omitempty"` // Optional element in create/replace queries +} + +func (r User) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Get the details of an Artifactory user +// Since: 2.4.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) GetUser(ctx context.Context, username string) (*User, *http.Response, error) { + path := fmt.Sprintf("/api/security/users/%s", username) + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeUser) + + user := new(User) + resp, err := s.client.Do(ctx, req, user) + return user, resp, err +} + +// Get the encrypted password of the authenticated requestor +// Since: 3.3.0 +// Security: Requires a privileged user +func (s *SecurityService) GetEncryptedPassword(ctx context.Context) (*string, *http.Response, error) { + path := "/api/security/encryptedPassword" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + return String(buf.String()), resp, err +} + +// Creates a new user in Artifactory or replaces an existing user +// Since: 2.4.0 +// Notes: Requires Artifactory Pro +// Missing values will be set to the default values as defined by the consumed type. +// Security: Requires an admin user +func (s *SecurityService) CreateOrReplaceUser(ctx context.Context, username string, user *User) (*http.Response, error) { + path := fmt.Sprintf("/api/security/users/%s", username) + req, err := s.client.NewJSONEncodedRequest("PUT", path, user) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} + +// Updates an exiting user in Artifactory with the provided user details. +// Since: 2.4.0 +// Notes: Requires Artifactory Pro +// Missing values will be set to the default values as defined by the consumed type +// Security: Requires an admin user +func (s *SecurityService) UpdateUser(ctx context.Context, username string, user *User) (*http.Response, error) { + path := fmt.Sprintf("/api/security/users/%s", username) + req, err := s.client.NewJSONEncodedRequest("POST", path, user) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} + +// Removes an Artifactory user. +// Since: 2.4.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) DeleteUser(ctx context.Context, username string) (*string, *http.Response, error) { + path := fmt.Sprintf("/api/security/users/%v", username) + req, err := s.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, nil, err + } + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// Expires a user's password +// Since: 4.4.2 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) ExpireUserPassword(ctx context.Context, username string) (*string, *http.Response, error) { + path := fmt.Sprintf("/api/security/users/authorization/expirePassword/%s", username) + req, err := s.client.NewRequest("POST", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// Expires password for a list of users +// Since: 4.4.2 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) ExpireMultipleUsersPassword(ctx context.Context, usernames []string) (*http.Response, error) { + path := "/api/security/users/authorization/expirePasswords" + req, err := s.client.NewJSONEncodedRequest("POST", path, usernames) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// Expires password for all users +// Since: 4.4.2 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) ExpireAllUsersPassword(ctx context.Context) (*http.Response, error) { + path := "/api/security/users/authorization/expirePasswordForAllUsers" + req, err := s.client.NewRequest("POST", path, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// Unexpires a user's password +// Since: 4.4.2 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) UnexpireUserPassword(ctx context.Context, username string) (*string, *http.Response, error) { + path := fmt.Sprintf("/api/security/users/authorization/unexpirePassword/%s", username) + req, err := s.client.NewRequest("POST", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +type PasswordChangeOptions struct { + Username *string `json:"username,omitempty"` + OldPassword *string `json:"oldPassword,omitempty"` + NewPassword1 *string `json:"newPassword1,omitempty"` + NewPassword2 *string `json:"newPassword2,omitempty"` +} + +// Changes a user's password +// Since: 4.4.2 +// Notes: Requires Artifactory Pro +// Security: Admin can apply this method to all users, and each (non-anonymous) user can use this method to change his own password. +func (s *SecurityService) ChangePassword(ctx context.Context, opts *PasswordChangeOptions) (*string, *http.Response, error) { + path := "/api/security/users/authorization/changePassword" + req, err := s.client.NewJSONEncodedRequest("POST", path, opts) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +type PasswordExpirationPolicy struct { + Enabled *bool `json:"enabled,omitempty"` + PasswordMaxAge *int `json:"passwordMaxAge,omitempty"` + NotifyByEmail *bool `json:"notifyByEmail,omitempty"` +} + +// Retrieves the password expiration policy +// Since: 4.4.2 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) GetPasswordExpirationPolicy(ctx context.Context) (*PasswordExpirationPolicy, *http.Response, error) { + path := "/api/security/configuration/passwordExpirationPolicy" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + v := new(PasswordExpirationPolicy) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +// Sets the password expiration policy +// Since: 4.4.2 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) SetPasswordExpirationPolicy(ctx context.Context, policy *PasswordExpirationPolicy) (*PasswordExpirationPolicy, *http.Response, error) { + path := "/api/security/configuration/passwordExpirationPolicy" + req, err := s.client.NewJSONEncodedRequest("PUT", path, policy) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + v := new(PasswordExpirationPolicy) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +type UserLockPolicy struct { + Enabled *bool `json:"enabled,omitempty"` + LoginAttempts *int `json:"loginAttempts,omitempty"` +} + +// Retrieves the currently configured user lock policy. +// Since: 4.4 +// Security: Requires a valid admin user +func (s *SecurityService) GetUserLockPolicy(ctx context.Context) (*UserLockPolicy, *http.Response, error) { + path := "/api/security/userLockPolicy" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + + req.Header.Set("Accept", mediaTypeJson) + + v := new(UserLockPolicy) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +// Configures the user lock policy that locks users out of their account if the number of repeated incorrect login attempts exceeds the configured maximum allowed. +// Since: 4.4 +// Security: Requires a valid admin user +func (s *SecurityService) SetUserLockPolicy(ctx context.Context, policy *PasswordExpirationPolicy) (*string, *http.Response, error) { + path := "/api/security/userLockPolicy" + req, err := s.client.NewJSONEncodedRequest("PUT", path, policy) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// If locking out users is enabled, lists all users that were locked out due to recurrent incorrect login attempts. +// Since: 4.4 +// Security: Requires a valid admin user +func (s *SecurityService) GetLockedOutUsers(ctx context.Context) ([]string, *http.Response, error) { + path := "/api/security/lockedUsers" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + var v []string + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +// Unlocks a single user that was locked out due to recurrent incorrect login attempts. +// Since: 4.4 +// Security: Requires a valid admin user +func (s *SecurityService) UnlockUser(ctx context.Context, username string) (*string, *http.Response, error) { + path := fmt.Sprintf("/api/security/unlockUsers/%s", username) + req, err := s.client.NewRequest("POST", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// Unlocks a list of users that were locked out due to recurrent incorrect login attempts. +// Since: 4.4 +// Security: Requires a valid admin user +func (s *SecurityService) UnlockMultipleUsers(ctx context.Context, usernames []string) (*string, *http.Response, error) { + path := "/api/security/unlockUsers" + req, err := s.client.NewJSONEncodedRequest("POST", path, usernames) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// Unlocks all users that were locked out due to recurrent incorrect login attempts. +// Since: 4.4 +// Security: Requires a valid admin user +func (s *SecurityService) UnlockedAllUsers(ctx context.Context) (*string, *http.Response, error) { + path := "/api/security/unlockAllUsers" + req, err := s.client.NewRequest("POST", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +type ApiKey struct { + ApiKey *string `json:"apiKey,omitempty"` +} + +// Create an API key for the current user. Returns an error if API key already exists - use regenerate API key instead. +// Since: 4.3.0 +func (s *SecurityService) CreateApiKey(ctx context.Context) (*ApiKey, *http.Response, error) { + path := "/api/security/apiKey" + req, err := s.client.NewRequest("POST", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + v := new(ApiKey) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +// Regenerate an API key for the current user +// Since: 4.3.0 +func (s *SecurityService) RegenerateApiKey(ctx context.Context) (*ApiKey, *http.Response, error) { + path := "/api/security/apiKey" + req, err := s.client.NewRequest("PUT", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + v := new(ApiKey) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +// Get the current user's own API key +// Since: 4.3.0 +func (s *SecurityService) GetApiKey(ctx context.Context) (*ApiKey, *http.Response, error) { + path := "/api/security/apiKey" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + v := new(ApiKey) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +// Revokes the current user's API key +// Since: 4.3.0 +func (s *SecurityService) RevokeApiKey(ctx context.Context) (*map[string]interface{}, *http.Response, error) { + path := "/api/security/apiKey" + req, err := s.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + v := new(map[string]interface{}) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +// Revokes the API key of another user +// Since: 4.3.0 +// Security: Requires a privileged user (Admin only) +func (s *SecurityService) RevokeUserApiKey(ctx context.Context, username string) (*map[string]interface{}, *http.Response, error) { + path := fmt.Sprintf("/api/security/apiKey/%s", username) + req, err := s.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + v := new(map[string]interface{}) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +// Revokes all API keys currently defined in the system +// Since: 4.3.0 +// Security: Requires a privileged user (Admin only) +func (s *SecurityService) RevokeAllApiKeys(ctx context.Context) (*map[string]interface{}, *http.Response, error) { + opt := struct { + DeleteAll int `json:"deleteAll"` + }{1} + path, err := addOptions("/api/security/apiKey", opt) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + v := new(map[string]interface{}) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +// application/vnd.org.jfrog.artifactory.security.Groups+json +type GroupDetails struct { + Name *string `json:"name,omitempty"` + Uri *string `json:"uri,omitempty"` +} + +func (r GroupDetails) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Get the groups list +// Since: 2.4.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) ListGroups(ctx context.Context) (*[]GroupDetails, *http.Response, error) { + path := "/api/security/groups" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeGroups) + + groups := new([]GroupDetails) + resp, err := s.client.Do(ctx, req, groups) + return groups, resp, err +} + +// application/vnd.org.jfrog.artifactory.security.Group+json +type Group struct { + Name *string `json:"name,omitempty"` // Optional element in create/replace queries + Description *string `json:"description,omitempty"` // Optional element in create/replace queries + AutoJoin *bool `json:"autoJoin,omitempty"` // Optional element in create/replace queries; default: false (must be false if adminPrivileges is true) + AdminPrivileges *bool `json:"adminPrivileges,omitempty"` // Optional element in create/replace queries; default: false + Realm *string `json:"realm,omitempty"` // Optional element in create/replace queries + RealmAttributes *string `json:"realmAttributes,omitempty"` // Optional element in create/replace queries +} + +func (r Group) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Get the details of an Artifactory Group +// Since: 2.4.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) GetGroup(ctx context.Context, groupName string) (*Group, *http.Response, error) { + path := fmt.Sprintf("/api/security/groups/%s", groupName) + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeGroup) + + group := new(Group) + resp, err := s.client.Do(ctx, req, group) + return group, resp, err +} + +// Creates a new group in Artifactory or replaces an existing group +// Since: 2.4.0 +// Notes: Requires Artifactory Pro +// Missing values will be set to the default values as defined by the consumed type. +// Security: Requires an admin user +func (s *SecurityService) CreateOrReplaceGroup(ctx context.Context, groupName string, group *Group) (*http.Response, error) { + url := fmt.Sprintf("/api/security/groups/%s", groupName) + req, err := s.client.NewJSONEncodedRequest("PUT", url, group) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} + +// Updates an exiting group in Artifactory with the provided group details. +// Since: 2.4.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) UpdateGroup(ctx context.Context, groupName string, group *Group) (*http.Response, error) { + path := fmt.Sprintf("/api/security/groups/%s", groupName) + req, err := s.client.NewJSONEncodedRequest("POST", path, group) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} + +// Removes an Artifactory group. +// Since: 2.4.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) DeleteGroup(ctx context.Context, groupName string) (*string, *http.Response, error) { + path := fmt.Sprintf("/api/security/groups/%v", groupName) + req, err := s.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, nil, err + } + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// application/vnd.org.jfrog.artifactory.security.PermissionTargets+json +type PermissionTargetsDetails struct { + Name *string `json:"name,omitempty"` + Uri *string `json:"uri,omitempty"` +} + +func (r PermissionTargetsDetails) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Get the permission targets list +// Since: 2.4.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) ListPermissionTargets(ctx context.Context) ([]*PermissionTargetsDetails, *http.Response, error) { + path := "/api/security/permissions" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePermissionTargets) + + var permissionTargets []*PermissionTargetsDetails + resp, err := s.client.Do(ctx, req, &permissionTargets) + return permissionTargets, resp, err +} + +type Principals struct { + Users *map[string][]string `json:"users,omitempty"` + Groups *map[string][]string `json:"groups,omitempty"` +} + +// application/vnd.org.jfrog.artifactory.security.PermissionTarget+json +// Permissions are set/returned according to the following conventions: +// m=admin; d=delete; w=deploy; n=annotate; r=read +type PermissionTargets struct { + Name *string `json:"name,omitempty"` // Optional element in create/replace queries + IncludesPattern *string `json:"includesPattern,omitempty"` // Optional element in create/replace queries + ExcludesPattern *string `json:"excludesPattern,omitempty"` // Optional element in create/replace queries + Repositories *[]string `json:"repositories,omitempty"` // Mandatory element in create/replace queries, optional in "update" queries + Principals *Principals `json:"principals,omitempty"` // Optional element in create/replace queries +} + +func (r PermissionTargets) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Get the details of an Artifactory Permission Target +// Since: 2.4.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) GetPermissionTargets(ctx context.Context, permissionName string) (*PermissionTargets, *http.Response, error) { + path := fmt.Sprintf("/api/security/permissions/%s", permissionName) + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePermissionTarget) + + permission := new(PermissionTargets) + resp, err := s.client.Do(ctx, req, permission) + return permission, resp, err +} + +// Creates a new permission target in Artifactory or replaces an existing permission target +// Since: 2.4.0 +// Notes: Requires Artifactory Pro +// Missing values will be set to the default values as defined by the consumed type. +// Security: Requires an admin user +func (s *SecurityService) CreateOrReplacePermissionTargets(ctx context.Context, permissionName string, permissionTargets *PermissionTargets) (*http.Response, error) { + path := fmt.Sprintf("/api/security/permissions/%s", permissionName) + req, err := s.client.NewJSONEncodedRequest("PUT", path, permissionTargets) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} + +// Deletes an Artifactory permission target. +// Since: 2.4.0 +// Notes: Requires Artifactory Pro +// Security: Requires an admin user +func (s *SecurityService) DeletePermissionTargets(ctx context.Context, permissionName string) (*string, *http.Response, error) { + path := fmt.Sprintf("/api/security/permissions/%v", permissionName) + req, err := s.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, nil, err + } + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// Permissions are returned according to the following conventions: +// m=admin; d=delete; w=deploy; n=annotate; r=read +type ItemPermissions struct { + Uri *string `json:"uri,omitempty"` + Principals *Principals `json:"principals,omitempty"` +} + +func (r ItemPermissions) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Returns a list of effective permissions for the specified item (file or folder). +// Only users and groups with some permissions on the item are returned. Supported by local and local-cached repositories. +// Since: 2.3.4 +// Notes: Requires Artifactory Pro +// Security: Requires a valid admin or local admin user. +func (s *SecurityService) GetEffectiveItemPermissions(ctx context.Context, repoName string, itemPath string) (*ItemPermissions, *http.Response, error) { + if !strings.HasPrefix(itemPath, "/") { + itemPath = itemPath[1:] + } + path := fmt.Sprintf("/api/storage/%s/%s?permissions", repoName, itemPath) + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeItemPermissions) + + itemPermissions := new(ItemPermissions) + resp, err := s.client.Do(ctx, req, itemPermissions) + return itemPermissions, resp, err +} + +// Retrieve the security configuration (security.xml). +// Since: 2.2.0 +// Notes: This is an advanced feature - make sure the new configuration is really what you wanted before saving. +// Security: Requires a valid admin us +func (s *SecurityService) GetSecurityConfiguration(ctx context.Context) (*string, *http.Response, error) { + path := "/api/system/security" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeXml) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// Creates a new Artifactory encryption key and activates Artifactory key encryption. +// Since: 3.2.2 +// Notes: This is an advanced feature intended for administrators +// Security: Requires a valid admin user +func (s *SecurityService) ActivateArtifactoryKeyEncryption(ctx context.Context) (*string, *http.Response, error) { + path := "/api/system/encrypt" + req, err := s.client.NewRequest("POST", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// Removes the current Artifactory encryption key and deactivates Artifactory key encryption. +// Since: 3.2.2 +// Notes: This is an advanced feature intended for administrators +// Security: Requires a valid admin user +func (s *SecurityService) DeactivateArtifactoryKeyEncryption(ctx context.Context) (*string, *http.Response, error) { + path := "/api/system/decrypt" + req, err := s.client.NewRequest("POST", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// Gets the public key that Artifactory provides to Debian and Opkg clients to verify packages +// Security: Requires an authenticated user, or anonymous (if "Anonymous Access" is globally enabled) +func (s *SecurityService) GetGPGPublicKey(ctx context.Context) (*string, *http.Response, error) { + path := "/api/gpg/key/public" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// Sets the public key that Artifactory provides to Debian and Opkg clients to verify packages +// Security: Requires a valid admin user +func (s *SecurityService) SetGPGPublicKey(ctx context.Context, gpgKey string) (*string, *http.Response, error) { + path := "/api/gpg/key/public" + req, err := s.client.NewRequest("PUT", path, strings.NewReader(gpgKey)) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-type", mediaTypePlain) + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// Sets the private key that Artifactory will use to sign Debian and ipk packages +// Security: Requires a valid admin user +func (s *SecurityService) SetGPGPrivateKey(ctx context.Context, gpgKey string) (*string, *http.Response, error) { + path := "/api/gpg/key/private" + req, err := s.client.NewRequest("PUT", path, strings.NewReader(gpgKey)) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-type", mediaTypePlain) + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// Sets the pass phrase required signing Debian and ipk packages using the private key +// Security: Requires a valid admin user +func (s *SecurityService) SetGPGPassPhrase(ctx context.Context, passphrase string) (*string, *http.Response, error) { + path := "/api/gpg/key/passphrase" + req, err := s.client.NewRequest("PUT", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("X-GPG-PASSPHRASE", passphrase) + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +type AccessTokenOptions struct { + // The grant type used to authenticate the request. In this case, the only value supported is "client_credentials" which is also the default value if this parameter is not specified. + GrantType *string `url:"grant_type,omitempty"` // [Optional, default: "client_credentials"] + // The user name for which this token is created. If the user does not exist, a transient user is created. Non-admin users can only create tokens for themselves so they must specify their own username. + // If the user does not exist, the member-of-groups scope token must be provided (e.g. member-of-groups: g1, g2, g3...) + Username *string `url:"username,omitempty"` + // The scope to assign to the token provided as a space-separated list of scope tokens. Currently there are three possible scope tokens: + // - "api:*" - indicates that the token grants access to REST API calls. This is always granted by default whether specified in the call or not. + // - member-of-groups:[] - indicates the groups that the token is associated with (e.g. member-of-groups: g1, g2, g3...). The token grants access according to the permission targets specified for the groups listed. + // Specify "*" for group-name to indicate that the token should provide the same access privileges that are given to the group of which the logged in user is a member. + // A non-admin user can only provide a scope that is a subset of the groups to which he belongs + // - "jfrt@:admin" - provides admin privileges on the specified Artifactory instance. This is only available for administrators. + // If omitted and the username specified exists, the token is granted the scope of that user. + Scope *string `url:"scope,omitempty"` // [Optional if the user specified in username exists] + // The time in seconds for which the token will be valid. To specify a token that never expires, set to zero. Non-admin can only set a value that is equal to or less than the default 3600. + ExpiresIn *int `url:"expires_in,omitempty"` // [Optional, default: 3600] + // If true, this token is refreshable and the refresh token can be used to replace it with a new token once it expires. + Refreshable *string `url:"refreshable,omitempty"` // [Optional, default: false] + // A space-separate list of the other Artifactory instances or services that should accept this token identified by their Artifactory Service IDs as obtained from the Get Service ID endpoint. + // In case you want the token to be accepted by all Artifactory instances you may use the following audience parameter "audience=jfrt@*". + Audience *string `url:"audience,omitempty"` // [Optional, default: Only the service ID of the Artifactory instance that created the token] +} + +type AccessToken struct { + AccessToken *string `json:"access_token,omitempty"` + ExpiresIn *int `json:"expires_in,omitempty"` + Scope *string `json:"scope,omitempty"` + TokenType *string `json:"token_type,omitempty"` + RefreshToken *string `json:"refresh_token,omitempty"` +} + +func (r AccessToken) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Creates an access token +// Since: 5.0.0 +// Security: Requires a valid user +func (s *SecurityService) CreateToken(ctx context.Context, opts *AccessTokenOptions) (*AccessToken, *http.Response, error) { + path := "/api/security/token" + req, err := s.client.NewURLEncodedRequest("POST", path, opts) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + token := new(AccessToken) + resp, err := s.client.Do(ctx, req, token) + return token, resp, err +} + +type AccessTokenRefreshOptions struct { + // Should be set to refresh_token. + GrantType *string `url:"grant_type,omitempty"` + // The refresh token of the access token that needs to be refreshed. + RefreshToken *string `url:"refresh_token,omitempty"` + // The access token to refresh. + AccessToken *string `url:"access_token,omitempty"` + // The user name for which this token is created. If the user does not exist, a transient user is created. Non-admin users can only create tokens for themselves so they must specify their own username. + // If the user does not exist, the member-of-groups scope token must be provided (e.g. member-of-groups: g1, g2, g3...) + // Note: access_token and username are mutually exclusive, so only one of these parameters should be specified. + Username *string `url:"username,omitempty"` + // The scope to assign to the token provided as a space-separated list of scope tokens. Currently there are three possible scope tokens: + // - "api:*" - indicates that the token grants access to REST API calls. This is always granted by default whether specified in the call or not. + // - member-of-groups:[] - indicates the groups that the token is associated with (e.g. member-of-groups: g1, g2, g3...). The token grants access according to the permission targets specified for the groups listed. + // Specify "*" for group-name to indicate that the token should provide the same access privileges that are given to the group of which the logged in user is a member. + // A non-admin user can only provide a scope that is a subset of the groups to which he belongs + // - "jfrt@:admin" - provides admin privileges on the specified Artifactory instance. This is only available for administrators. + // If omitted and the username specified exists, the token is granted the scope of that user. + Scope *string `url:"scope,omitempty"` + // The time in seconds for which the token will be valid. To specify a token that never expires, set to zero. Non-admin can only set a value that is equal to or less than the default 3600. + ExpiresIn *int `url:"expires_in,omitempty"` + // If true, this token is refreshable and the refresh token can be used to replace it with a new token once it expires. + Refreshable *string `url:"refreshable,omitempty"` + // A space-separate list of the other Artifactory instances or services that should accept this token identified by their Artifactory Service IDs as obtained from the Get Service ID endpoint. + // In case you want the token to be accepted by all Artifactory instances you may use the following audience parameter "audience=jfrt@*". + Audience *string `url:"audience,omitempty"` +} + +// Refresh an access token to extend its validity. If only the access token and the refresh token are provided (and no other parameters), this pair is used for authentication. If username or any other parameter is provided, then the request must be authenticated by a token that grants admin permissions. +// Since: 5.0.0 +// Security: Requires a valid user (unless both access token and refresh token are provided) +func (s *SecurityService) RefreshToken(ctx context.Context, opts *AccessTokenRefreshOptions) (*AccessToken, *http.Response, error) { + path := "/api/security/token" + req, err := s.client.NewURLEncodedRequest("POST", path, opts) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + token := new(AccessToken) + resp, err := s.client.Do(ctx, req, token) + return token, resp, err +} + +type AccessTokenRevokeOptions struct { + Token string `url:"token,omitempty"` +} + +// Revoke an access token +// Since: 5.0.0 +// Security: Requires a valid user +func (s *SecurityService) RevokeToken(ctx context.Context, opts AccessTokenRevokeOptions) (*string, *http.Response, error) { + path := "/api/security/token/revoke" + req, err := s.client.NewURLEncodedRequest("POST", path, opts) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// Provides the service ID of an Artifactory instance or cluster. Up to version 5.5.1, the Artiafctory service ID is formatted jf-artifactory@. From version 5.5.2 the service ID is formatted jfrt@. +// Since: 5.0.0 +// Security: Requires an admin user +func (s *SecurityService) GetServiceId(ctx context.Context) (*string, *http.Response, error) { + path := "/api/system/service_id" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +type CertificateDetails struct { + CertificateAlias *string `json:"certificateAlias,omitempty"` + IssuedTo *string `json:"issuedTo,omitempty"` + IssuedBy *string `json:"issuedby,omitempty"` + IssuedOn *string `json:"issuedOn,omitempty"` + ValidUntil *string `json:"validUntil,omitempty"` + FingerPrint *string `json:"fingerPrint,omitempty"` +} + +func (r CertificateDetails) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Returns a list of installed SSL certificates. +// Since:5.4.0 +// Security: Requires an admin user +func (s *SecurityService) GetCertificates(ctx context.Context) (*[]CertificateDetails, *http.Response, error) { + path := "/api/system/security/certificates" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + certificates := new([]CertificateDetails) + resp, err := s.client.Do(ctx, req, certificates) + return certificates, resp, err +} + +// Adds an SSL certificate. +// Since:5.4.0 +// Security: Requires an admin user +func (s *SecurityService) AddCertificate(ctx context.Context, alias string, pem *os.File) (*Status, *http.Response, error) { + path := fmt.Sprintf("/api/system/security/certificates/%s", alias) + req, err := s.client.NewRequest("POST", path, pem) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-type", mediaTypePlain) + req.Header.Set("Accept", mediaTypeJson) + + status := new(Status) + resp, err := s.client.Do(ctx, req, status) + return status, resp, err +} + +// Deletes an SSL certificate. +// Since:5.4.0 +// Security: Requires an admin user +func (s *SecurityService) DeleteCertificate(ctx context.Context, alias string) (*Status, *http.Response, error) { + path := fmt.Sprintf("/api/system/security/certificates/%s", alias) + req, err := s.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + status := new(Status) + resp, err := s.client.Do(ctx, req, status) + return status, resp, err +} diff --git a/pkg/artifactory/security_test.go b/pkg/artifactory/security_test.go new file mode 100644 index 0000000..a60755a --- /dev/null +++ b/pkg/artifactory/security_test.go @@ -0,0 +1 @@ +package artifactory diff --git a/pkg/artifactory/system.go b/pkg/artifactory/system.go new file mode 100644 index 0000000..4dd8c5d --- /dev/null +++ b/pkg/artifactory/system.go @@ -0,0 +1,328 @@ +package artifactory + +import ( + "bytes" + "context" + "encoding/json" + "net/http" +) + +type SystemService service + +// System Info +// Get general system information. +// Since: 2.2.0 +// Security: Requires a valid admin user +func (s *SystemService) GetSystemInfo(ctx context.Context) (*string, *http.Response, error) { + path := "/api/system" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// Get a simple status response about the state of Artifactory +// Returns 200 code with an 'OK' text if Artifactory is working properly, if not will return an HTTP error code with a reason. +// Since: 2.3.0 +// Security: Requires a valid user (can be anonymous). If artifactory.ping.allowUnauthenticated=true is set in +// artifactory.system.properties, then no authentication is required even if anonymous access is disabled. +func (s *SystemService) Ping(ctx context.Context) (*string, *http.Response, error) { + path := "/api/system/ping" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePlain) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +type VerifyConnectionOptions struct { + Endpoint *string `json:"endpoint,omitempty"` // Mandatory + Username *string `json:"username,omitempty"` // Optional + Password *string `json:"password,omitempty"` // Optional +} + +// Verifies a two-way connection between Artifactory and another product +// Returns Success (200) if Artifactory receives a similar success code (200) from the provided endpoint. +// Upon error, returns 400 along with a JSON object that contains the error returned from the other system. +// Since: 4.15.0 +// Security: Requires an admin user. +func (s *SystemService) VerifyConnection(ctx context.Context, opt *VerifyConnectionOptions) (*string, *http.Response, error) { + url := "/api/system/verifyconnection" + + req, err := s.client.NewJSONEncodedRequest("POST", url, opt) + if err != nil { + return nil, nil, err + } + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + + return String(buf.String()), resp, nil +} + +// Get the general configuration (artifactory.config.xml). +// Since: 2.2.0 +// Security: Requires a valid admin user +func (s *SystemService) GetConfiguration(ctx context.Context) (*string, *http.Response, error) { + path := "/api/system/configuration" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeXml) + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return nil, resp, err + } + return String(buf.String()), resp, nil +} + +// Changes the Custom URL base +// Since: 3.9.0 +// Security: Requires a valid admin user +func (s *SystemService) UpdateUrlBase(ctx context.Context, newUrl string) (*http.Response, error) { + path := "/api/system/configuration/baseUrl" + req, err := s.client.NewJSONEncodedRequest("PUT", path, newUrl) + if err != nil { + return nil, err + } + req.Header.Set("Content-type", mediaTypePlain) + + return s.client.Do(ctx, req, nil) +} + +type LicenseDetails struct { + Type *string `json:"type,omitempty"` + ValidThrough *string `json:"validThrough,omitempty"` + LicensedTo *string `json:"licensedTo,omitempty"` +} + +func (r LicenseDetails) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Retrieve information about the currently installed license. +// Since: 3.3.0 +// Security: Requires a valid admin user +func (s *SystemService) GetLicense(ctx context.Context) (*LicenseDetails, *http.Response, error) { + path := "/api/system/license" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + v := new(LicenseDetails) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +type LicenseKey struct { + LicenseKey *string `json:"licenseKey,omitempty"` +} + +// Install new license key or change the current one. +// Since: 3.3.0 +// Security: Requires a valid admin user +func (s *SystemService) InstallLicense(ctx context.Context, licenseKey *LicenseKey) (*Status, *http.Response, error) { + path := "/api/system/licenses" + req, err := s.client.NewJSONEncodedRequest("POST", path, licenseKey) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + v := new(Status) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +type HALicense struct { + Type *string `json:"type,omitempty"` + ValidThrough *string `json:"validThrough,omitempty"` // validity date formatted MMM DD, YYYY + LicensedTo *string `json:"licensedTo,omitempty"` + LicenseHash *string `json:"licenseHash,omitempty"` + NodeId *string `json:"nodeId,omitempty"` // Node ID of the node activated with this license | Not in use + NodeUrl *string `json:"nodeUrl,omitempty"` // URL of the node activated with this license | Not in use + Expired *bool `json:"expired,omitempty"` +} + +type HALicenses struct { + Licenses *[]HALicense `json:"licenses,omitempty"` +} + +func (r HALicenses) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Retrieve information about the currently installed licenses in an HA cluster. +// Since: 5.0.0 +// Security: Requires a valid admin user +func (s *SystemService) ListHALicenses(ctx context.Context) (*HALicenses, *http.Response, error) { + path := "/api/system/licenses" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + v := new(HALicenses) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +// Install a new license key(s) on an HA cluster. +// Since: 5.0.0 +// Security: Requires an admin user +func (s *SystemService) InstallHALicenses(ctx context.Context, licenses []LicenseKey) (*Status, *http.Response, error) { + path := "/api/system/licenses" + req, err := s.client.NewJSONEncodedRequest("POST", path, licenses) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + v := new(Status) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +type HALicenseHashes struct { + LicenseHash *[]string `url:"licenseHash,omitempty"` +} + +// Deletes a license key from an HA cluster. +// Since: 5.0.0 +// Security: Requires an admin user +func (s *SystemService) DeleteHALicenses(ctx context.Context, licenseHashes HALicenseHashes) (*Status, *http.Response, error) { + path, err := addOptions("/api/system/licenses", licenseHashes) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + v := new(Status) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +type VersionAddOns struct { + Version *string `json:"version,omitempty"` + Revision *string `json:"revision,omitempty"` + Addons *[]string `json:"addons,omitempty"` +} + +func (r VersionAddOns) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Retrieve information about the current Artifactory version, revision, and currently installed Add-ons +// Since: 2.2.2 +// Security: Requires a valid user (can be anonymous) +func (s *SystemService) GetVersionAndAddons(ctx context.Context) (*VersionAddOns, *http.Response, error) { + path := "/api/system/version" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeSystemVersion) + + v := new(VersionAddOns) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +type ReverseProxyConfig struct { + Key *string `json:"key,omitempty"` + WebServerType *string `json:"webServerType,omitempty"` + ArtifactoryAppContext *string `json:"artifactoryAppContext,omitempty"` + PublicAppContext *string `json:"publicAppContext,omitempty"` + ServerName *string `json:"serverName,omitempty"` + ServerNameExpression *string `json:"serverNameExpression,omitempty"` + ArtifactoryServerName *string `json:"artifactoryServerName,omitempty"` + ArtifactoryPort *int `json:"artifactoryPort,omitempty"` + SslCertificate *string `json:"sslCertificate,omitempty"` + SslKey *string `json:"sslKey,omitempty"` + DockerReverseProxyMethod *string `json:"dockerReverseProxyMethod,omitempty"` + UseHttps *bool `json:"useHttps,omitempty"` + UseHttp *bool `json:"useHttp,omitempty"` + SslPort *int `json:"sslPort,omitempty"` + HttpPort *int `json:"httpPort,omitempty"` +} + +func (r ReverseProxyConfig) String() string { + res, _ := json.MarshalIndent(r, "", " ") + return string(res) +} + +// Retrieves the reverse proxy configuration +// Since: 4.3.1 +// Security: Requires a valid admin user +func (s *SystemService) GetReverseProxyConfig(ctx context.Context) (*ReverseProxyConfig, *http.Response, error) { + path := "/api/system/configuration/webServer" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeJson) + + v := new(ReverseProxyConfig) + resp, err := s.client.Do(ctx, req, v) + return v, resp, err +} + +// Updates the reverse proxy configuration +// Since: 4.3.1 +// Security: Requires an admin user +func (s *SystemService) UpdateReverseProxyConfig(ctx context.Context, config *ReverseProxyConfig) (*http.Response, error) { + path := "/api/system/configuration/webServer" + req, err := s.client.NewJSONEncodedRequest("POST", path, config) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// Gets the reverse proxy configuration snippet in text format +// Since: 4.3.1 +// Security: Requires a valid user (not anonymous) +func (s *SystemService) GetReverseProxySnippet(ctx context.Context) (*string, *http.Response, error) { + path := "/api/system/configuration/reverseProxy/nginx" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypePlain) + + v := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, v) + return String(v.String()), resp, err +} diff --git a/pkg/artifactory/system_test.go b/pkg/artifactory/system_test.go new file mode 100644 index 0000000..a60755a --- /dev/null +++ b/pkg/artifactory/system_test.go @@ -0,0 +1 @@ +package artifactory