pax_global_header00006660000000000000000000000064145726312300014515gustar00rootroot0000000000000052 comment=26cfeb6081fd2095d0ab06d3def135e3413e8d47 golang-github-canonical-candid-1.12.3/000077500000000000000000000000001457263123000174755ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/.github/000077500000000000000000000000001457263123000210355ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/.github/ISSUE_TEMPLATE/000077500000000000000000000000001457263123000232205ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/.github/ISSUE_TEMPLATE/bug.md000066400000000000000000000002441457263123000243170ustar00rootroot00000000000000# Issue: Bug ## Expected Behavior ## Actual Behavior ## Steps to Reproduce the Problem 1. 2. 3. ## Specifications - Version: - Platform: - Subsystem:golang-github-canonical-candid-1.12.3/.github/ISSUE_TEMPLATE/feature.md000066400000000000000000000004001457263123000251670ustar00rootroot00000000000000# Issue: Feature > Note: If you believe this feature request would be a small change, please feel free to propose a PR, which will be reviewed as soon as possible. ## Feature description golang-github-canonical-candid-1.12.3/.github/ISSUE_TEMPLATE/support.md000066400000000000000000000003421457263123000252550ustar00rootroot00000000000000# Issue: Support ## Ask your question ## Please provide any additional details to help us answer your question golang-github-canonical-candid-1.12.3/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000012251457263123000246360ustar00rootroot00000000000000## Description The what and why - include a summary of the change, describe what it does, and include relevant motivation and context. ## Engineering checklist *Check only items that apply* - [ ] Documentation updated - [ ] Covered by unit tests - [ ] Covered by integration tests - [ ] Independent change* ## Test instructions ## Notes for code reviewers golang-github-canonical-candid-1.12.3/.github/dependabot.yaml000066400000000000000000000004771457263123000240360ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: # Check for updates to GitHub Actions every weekday interval: "daily" - package-ecosystem: "gomod" directory: "/" schedule: # Check for updates to go modules every weekday interval: "daily" golang-github-canonical-candid-1.12.3/.github/workflows/000077500000000000000000000000001457263123000230725ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/.github/workflows/charm.yaml000066400000000000000000000014001457263123000250430ustar00rootroot00000000000000name: BuildCharm on: workflow_dispatch: inputs: charm: description: "Charm to build." default: "candid" required: false type: choice options: - candid - candid-k8s env: GH_USER: ${{ secrets.GH_USER }} GH_AUTH: ${{ secrets.GH_AUTH }} jobs: build-charm: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 - run: git fetch --prune --unshallow - run: sudo snap install charmcraft --channel=2.x/stable --classic - run: charmcraft pack --project-dir ./charms/${{ inputs.charm }} --destructive-mode - uses: actions/upload-artifact@v3 with: name: ${{ inputs.charm }}-charm path: ./*.charm if-no-files-found: error golang-github-canonical-candid-1.12.3/.github/workflows/ci.yaml000066400000000000000000000034101457263123000243470ustar00rootroot00000000000000name: CI on: push: pull_request: workflow_dispatch: env: GH_AUTH: ${{ secrets.GH_AUTH }} GH_USER: ${{ secrets.GH_USER }} jobs: lint: runs-on: ubuntu-20.04 continue-on-error: true steps: - uses: actions/checkout@v3 - uses: ./.github/workflows/setupgo118amd64 with: user: ${{ secrets.GH_USER }} pat: ${{ secrets.GH_AUTH }} - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: version: latest only-new-issues: true skip-pkg-cache: true skip-build-cache: true build_test: name: Build and Test needs: - lint runs-on: ubuntu-20.04 services: postgres: image: postgres env: POSTGRES_PASSWORD: password ports: - 5432:5432 # Set health checks to wait until postgres has started options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 mongo: image: mongo:3.6-xenial options: >- --health-cmd "mongo test --quiet --eval 'quit(db.runCommand(\"ping\").ok ? 0 : 1)'" --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 27017:27017 steps: - uses: actions/checkout@v3 - name: Install dependencies run: sudo apt-get update -y && sudo apt-get install -y gcc git-core - uses: ./.github/workflows/setupgo118amd64 - name: Build and Test run: go test -mod readonly ./... env: MGOCONNECTIONSTRING: localhost:27017 PGHOST: localhost PGPASSWORD: password PGSSLMODE: disable PGUSER: postgres PGPORT: 5432 golang-github-canonical-candid-1.12.3/.github/workflows/oci-image.yaml000066400000000000000000000020001457263123000256000ustar00rootroot00000000000000name: BuildOCIImage on: workflow_dispatch: jobs: candid-oci-image: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: git fetch --prune --unshallow - uses: ./.github/workflows/setupgo118amd64 - uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 - name: Setup version and commit run: | echo "GIT_COMMIT=$(git rev-parse --verify HEAD)" >> $GITHUB_ENV echo "VERSION=$(git describe --dirty)" >> $GITHUB_ENV - name: Build image uses: docker/build-push-action@v4 with: context: . file: ./Dockerfile target: deploy-env tags: candid:latest build-args: | GIT_COMMIT=${{ env.GIT_COMMIT }} VERSION=${{ env.VERSION }} outputs: | type=docker,dest=candid-image.tar - uses: actions/upload-artifact@v3 with: name: candid-image path: ./*.tar if-no-files-found: error golang-github-canonical-candid-1.12.3/.github/workflows/setupgo118amd64/000077500000000000000000000000001457263123000256465ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/.github/workflows/setupgo118amd64/action.yaml000066400000000000000000000011241457263123000300050ustar00rootroot00000000000000name: "Setup go and vendor" description: "Setup go with our auth" inputs: user: description: "User to authenticate against" required: true default: "" pat: description: "PAT token to authenticate against" required: true default: "" runs: using: "composite" steps: - uses: actions/setup-go@v3 with: go-version: 1.18 architecture: x64 cache: true - run: git config --global url.https://${{ inputs.user }}:${{ inputs.pat }}@github.com/.insteadOf https://github.com/ shell: bash - run: go mod vendor shell: bash golang-github-canonical-candid-1.12.3/.github/workflows/snap.yaml000066400000000000000000000012101457263123000247110ustar00rootroot00000000000000name: BuildSnap on: workflow_dispatch: env: GH_AUTH: ${{ secrets.GH_AUTH }} GH_USER: ${{ secrets.GH_USER }} jobs: build-snap: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 - run: git fetch --prune --unshallow - uses: ./.github/workflows/setupgo118amd64 with: user: ${{ secrets.GH_USER }} pat: ${{ secrets.GH_AUTH }} - run: sudo snap install snapcraft --channel=7.x/stable --classic - run: snapcraft --destructive-mode - uses: actions/upload-artifact@v3 with: name: candid-snap path: ./*.snap if-no-files-found: error golang-github-canonical-candid-1.12.3/.gitignore000066400000000000000000000002361457263123000214660ustar00rootroot00000000000000access.log cmd/idserver/idserver /candidsrv version/init.go candid-*.tar.xz candid_source.tar.bz2 parts/ prime/ stage/ snap/.snapcraft/ *.assert *.snap *.swp golang-github-canonical-candid-1.12.3/Dockerfile000066400000000000000000000021271457263123000214710ustar00rootroot00000000000000# syntax=docker/dockerfile:1.3.1 FROM ubuntu:20.04 as build-env ARG GIT_COMMIT ARG VERSION ARG GO_VERSION ARG ARCH WORKDIR /usr/src/candid SHELL ["/bin/bash", "-c"] RUN apt update && apt install wget git -y RUN wget -L "https://golang.org/dl/go${GO_VERSION}.linux-${ARCH}.tar.gz" RUN tar -C /usr/local -xzf "go${GO_VERSION}.linux-${ARCH}.tar.gz" ENV PATH="${PATH}:/usr/local/go/bin" COPY . . RUN go build -o candidsrv -v ./cmd/candidsrv RUN go build -o candid -v ./cmd/candid RUN GOBIN=/usr/src/candid go install gopkg.in/macaroon-bakery.v2/cmd/bakery-keygen@latest # Define a smaller single process image for deployment FROM ubuntu:20.04 AS deploy-env RUN apt-get -qq update && apt-get -qq install -y ca-certificates curl WORKDIR /root/ RUN mkdir www RUN mkdir logs COPY --from=build-env /usr/src/candid/candidsrv . COPY --from=build-env /usr/src/candid/candid . COPY --from=build-env /usr/src/candid/bakery-keygen . COPY --from=build-env /usr/src/candid/static ./www/static/ COPY --from=build-env /usr/src/candid/templates ./www/templates RUN touch config.yaml ENTRYPOINT ["./candidsrv"] CMD ["config.yaml"] golang-github-canonical-candid-1.12.3/LICENCE.client000066400000000000000000000215061457263123000217430ustar00rootroot00000000000000Client packages in this repository are licensed as follows. If you contribute to these packages, it is assumed that you license your contribution under the same license unless you state otherwise. All files Copyright (C) 2015 Canonical Ltd. unless otherwise specified in the file. This software is licensed under the LGPLv3, included below. As a special exception to the GNU Lesser General Public License version 3 ("LGPL3"), the copyright holders of this Library give you permission to convey to a third party a Combined Work that links statically or dynamically to this Library without providing any Minimal Corresponding Source or Minimal Application Code as set out in 4d or providing the installation information set out in section 4e, provided that you comply with the other provisions of LGPL3 and provided that you meet, for the Application the terms and conditions of the license(s) which apply to the Application. Except as stated in this special exception, the provisions of LGPL3 will continue to comply in full to this Library. If you modify this Library, you may apply this exception to your version of this Library, but you are not obliged to do so. If you do not wish to do so, delete this exception statement from your version. This exception does not (and cannot) modify any license terms which apply to the Application, with which you must still comply. GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. golang-github-canonical-candid-1.12.3/LICENSE000066400000000000000000001033301457263123000205020ustar00rootroot00000000000000 GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . golang-github-canonical-candid-1.12.3/Makefile000066400000000000000000000050131457263123000211340ustar00rootroot00000000000000# Copyright 2014 Canonical Ltd. # Licensed under the AGPLv3, see LICENCE file for details. # Makefile for the candid identity service. GIT_COMMIT := $(shell git rev-parse --verify HEAD) GIT_VERSION := $(shell git describe --dirty) GO_VERSION := $(shell go list -f {{.GoVersion}} -m) ARCH := $(shell dpkg --print-architecture) DEPENDENCIES := build-essential bzr SNAP_DEPENDENCIES := go snapcraft default: build build: version/init.go go build ./... check: version/init.go go test ./... install: version/init.go go install $(INSTALL_FLAGS) -v ./... clean: go clean ./... -$(RM) version/init.go -snapcraft clean # Reformat source files. format: gofmt -w -l . # Reformat and simplify source files. simplify: gofmt -w -l -s . candidsrv: version/init.go go build ./cmd/candidsrv # Run the candid server. server: candidsrv ./candidsrv cmd/candidsrv/config.yaml # Generate version information version/init.go: version/init.go.tmpl FORCE gofmt -r "unknownVersion -> Version{GitCommit: \"${GIT_COMMIT}\", Version: \"${GIT_VERSION}\",}" $< >$@ # Generate snaps candid_$(GIT_VERSION)_$(ARCH).snap: snapcraft RELEASE_BINARY_PACKAGES=./cmd/candidsrv .PHONY: deploy deploy: candid_$(GIT_VERSION)_$(ARCH).snap $(MAKE) -C charm build juju deploy -v ./charm/build/candid --resource candid=candid_$(GIT_VERSION)_$(ARCH).snap # Install packages required to develop the candid service and run tests. APT_BASED := $(shell command -v apt-get >/dev/null; echo $$?) sysdeps: ifeq ($(APT_BASED),0) @echo Installing dependencies @sudo apt-get update @sudo apt-get -y install $(DEPENDENCIES) @sudo snap install $(SNAP_DEPENDENCIES) else @echo sysdeps runs only on systems with apt-get @echo on OS X with homebrew try: brew install bazaar mongodb endif image: DOCKER_BUILDKIT=1 \ docker build \ --build-arg="GIT_COMMIT=$(GIT_COMMIT)" \ --build-arg="VERSION=$(GIT_VERSION)" \ --build-arg="GO_VERSION=$(GO_VERSION)" \ --build-arg="ARCH=$(ARCH)" \ . -f ./Dockerfile -t candid help: @echo -e 'Identity service - list of make targets:\n' @echo 'make - Build the package.' @echo 'make check - Run tests.' @echo 'make install - Install the package.' @echo 'make server - Start the candid server.' @echo 'make clean - Remove object files from package source directories.' @echo 'make sysdeps - Install the development environment system packages.' @echo 'make format - Format the source files.' @echo 'make simplify - Format and simplify the source files.' .PHONY: build check install clean format server simplify snap sysdeps help FORCE FORCE: golang-github-canonical-candid-1.12.3/README.md000066400000000000000000000035251457263123000207610ustar00rootroot00000000000000# Candid Identity service The Candid server provides a macaroon-based authentication service. ## Installation The easiest way to start using the candid service is with the snap: snap install candid The configuration file used by the snap can be found in `/var/snap/candid/current/config.yaml`. ## Development ### Requirements Candid requires go1.11 or later to build. This is available in the go snap: snap install go Go will additionally require installing the following packages in order that it can fetch and build candid dependencies: apt install build-essential bzr git ### Source Get the source from `github.com/canonical/candid`. git clone https://github.com/canonical/candid It is recommended that you check out the source outside of any `$GOPATH` (`$HOME/go` by default). If you do wish to check out into a `$GOPATH` then you will need to set the environment variable `GO111MODULE=on`. ### Testing The `store/mgostore` component additionally requires a running mongodb server, this may be running on a different system. The location of the mongodb server should be specified in an environment variable called `MGOCONNECTIONSTRING`, if this does not exist then the standard port (27017) on localhost will be assumed. To disable testing of `store/mgostore` completely then set the environment variable `MGOTESTDISABLE=1`. The `store/sqlstore` component additionally requires a running postgresql, this may be running on a different system. The posgresql system to use is specified using the standard postgresql [environment variables](https://www.postgresql.org/docs/10/static/libpq-envars.html). To skip running postgresql tests set the environment variable `PGTESTDISABLE=1`. Tests are run by running make check in the root of the source tree. The tests for a single package can be run by running `go test` in the package directory. golang-github-canonical-candid-1.12.3/candidclient/000077500000000000000000000000001457263123000221165ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/candidclient/client.go000066400000000000000000000157161457263123000237350ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package candidclient import ( "context" "net/http" "net/url" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/canonical/candid/params" ) // Note: tests for this code are in the server implementation. const ( // Production holds the URL of the production jujucharms candid // server. Production = "https://api.jujucharms.com/identity" // Staging holds the URL of the staging jujucharms candid server. Staging = "https://api.staging.jujucharms.com/identity" ) // Client represents the client of an identity server. // It implements the identchecker.IdentityClient interface, so can // be used directly to provide authentication for macaroon-based // services. type Client struct { client // permChecker is used to check group membership. // It is only non-zero when groups are enabled. permChecker *PermChecker useUserID bool } var _ identchecker.IdentityClient = (*Client)(nil) // NewParams holds the parameters for creating a new client. type NewParams struct { // BaseURL holds the URL of the identity manager. BaseURL string // Client holds the client to use to make requests // to the identity manager. Client *httpbakery.Client // AgentUsername holds the username for group-fetching authorization. // If this is empty, no group information will be provided. // The agent key is expected to be held inside the Client. AgentUsername string // CacheTime holds the maximum duration for which // group membership information will be cached. // If this is zero, group membership information will not be cached. CacheTime time.Duration // If UseUserID is true then the macaroons will use unique user // ID to transfer identity information rather than usernames. UseUserID bool } // New returns a new client. func New(p NewParams) (*Client, error) { var c Client _, err := url.Parse(p.BaseURL) if p.BaseURL == "" || err != nil { return nil, errgo.Newf("bad identity client base URL %q", p.BaseURL) } c.Client.BaseURL = p.BaseURL if p.AgentUsername != "" { if err := agent.SetUpAuth(p.Client, &agent.AuthInfo{ Key: p.Client.Key, Agents: []agent.Agent{{ URL: p.BaseURL, Username: p.AgentUsername, }}, }); err != nil { return nil, errgo.Notef(err, "cannot set up agent authentication") } c.permChecker = NewPermChecker(&c, p.CacheTime) } c.Client.Doer = p.Client c.Client.UnmarshalError = httprequest.ErrorUnmarshaler(new(params.Error)) c.useUserID = p.UseUserID return &c, nil } // IdentityFromContext implements identchecker.IdentityClient.IdentityFromContext // by returning caveats created by IdentityCaveats. func (c *Client) IdentityFromContext(ctx context.Context) (identchecker.Identity, []checkers.Caveat, error) { if c.useUserID { return nil, IdentityUserIDCaveats(c.Client.BaseURL), nil } return nil, IdentityCaveats(c.Client.BaseURL), nil } // DeclaredIdentity implements IdentityClient.DeclaredIdentity. // On success, it returns a value that implements Identity as // well as identchecker.Identity. func (c *Client) DeclaredIdentity(ctx context.Context, declared map[string]string) (identchecker.Identity, error) { if c.useUserID { return c.declaredUserIDIdentity(ctx, declared) } username := declared["username"] if username == "" { return nil, errgo.Newf("no declared user name in %q", declared) } return &usernameIdentity{ client: c, username: username, }, nil } func (c *Client) declaredUserIDIdentity(ctx context.Context, declared map[string]string) (identchecker.Identity, error) { userid := declared["userid"] if userid == "" { return nil, errgo.Newf("no declared user id in %q", declared) } return &useridIdentity{ client: c, user: params.User{ ExternalID: userid, }, }, nil } // CacheEvict evicts username from the user info cache. func (c *Client) CacheEvict(username string) { if c.permChecker != nil { c.permChecker.CacheEvict(username) } } // CacheEvictAll evicts everything from the user info cache. func (c *Client) CacheEvictAll() { if c.permChecker != nil { c.permChecker.CacheEvictAll() } } // LoginMethods returns information about the available login methods // for the given URL, which is expected to be a URL as passed to // a VisitWebPage function during the macaroon bakery discharge process. func LoginMethods(client *http.Client, u *url.URL) (*params.LoginMethods, error) { req, err := http.NewRequest("GET", u.String(), nil) if err != nil { return nil, errgo.Notef(err, "cannot create request") } req.Header.Set("Accept", "application/json") resp, err := client.Do(req) if err != nil { return nil, errgo.Notef(err, "cannot do request") } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { var herr httpbakery.Error if err := httprequest.UnmarshalJSONResponse(resp, &herr); err != nil { return nil, errgo.Notef(err, "cannot unmarshal error") } return nil, &herr } var lm params.LoginMethods if err := httprequest.UnmarshalJSONResponse(resp, &lm); err != nil { return nil, errgo.Notef(err, "cannot unmarshal login methods") } return &lm, nil } // IdentityCaveats returns a slice containing a third party // "is-authenticated-user" caveat addressed to the identity server at // the given URL that will authenticate the user with discharged. The // user can be determined by calling Client.DeclaredIdentity on the // declarations made by the discharge macaroon, func IdentityCaveats(url string) []checkers.Caveat { return []checkers.Caveat{ checkers.NeedDeclaredCaveat( checkers.Caveat{ Location: url, Condition: "is-authenticated-user", }, "username", ), } } // UserDeclaration returns a first party caveat that can be used // by an identity manager to declare an identity on a discharge // macaroon. func UserDeclaration(username string) checkers.Caveat { return checkers.DeclaredCaveat("username", username) } // IdentityUserIDCaveats returns a slice containing a third party // "is-authenticated-userid" caveat addressed to the identity server at // the given URL that will authenticate the user with discharged. The // user can be determined by calling Client.DeclaredIdentity on the // declarations made by the discharge macaroon, func IdentityUserIDCaveats(url string) []checkers.Caveat { return []checkers.Caveat{ checkers.NeedDeclaredCaveat( checkers.Caveat{ Location: url, Condition: "is-authenticated-userid", }, "userid", ), } } // UserIDDeclaration returns a first party caveat that can be used by an // identity manager to declare an identity on a discharge macaroon. func UserIDDeclaration(id string) checkers.Caveat { return checkers.DeclaredCaveat("userid", id) } //go:generate httprequest-generate-client ../internal/v1 handler client golang-github-canonical-candid-1.12.3/candidclient/client_generated.go000066400000000000000000000151031457263123000257410ustar00rootroot00000000000000// The code in this file was automatically generated by running httprequest-generate-client. // DO NOT EDIT package candidclient import ( "context" "github.com/canonical/candid/params" "gopkg.in/httprequest.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" ) type client struct { Client httprequest.Client } // ClearUserMFACredentials removes all MFA credentials for a user. func (c *client) ClearUserMFACredentials(ctx context.Context, p *params.ClearUserMFACredentialsRequest) error { return c.Client.Call(ctx, p, nil) } // CreateAgent creates a new agent and returns the newly chosen username // for the agent. func (c *client) CreateAgent(ctx context.Context, p *params.CreateAgentRequest) (*params.CreateAgentResponse, error) { var r *params.CreateAgentResponse err := c.Client.Call(ctx, p, &r) return r, err } // DeleteSSHKeys removes all of the ssh keys specified from the keys // stored for the given user. It is not an error to attempt to remove a // key that is not associated with the user. func (c *client) DeleteSSHKeys(ctx context.Context, p *params.DeleteSSHKeysRequest) error { return c.Client.Call(ctx, p, nil) } // DischargeTokenForUser allows an administrator to create a discharge // token for the specified user. func (c *client) DischargeTokenForUser(ctx context.Context, p *params.DischargeTokenForUserRequest) (params.DischargeTokenForUserResponse, error) { var r params.DischargeTokenForUserResponse err := c.Client.Call(ctx, p, &r) return r, err } // GetSSHKeys returns any SSH keys stored for the given user. func (c *client) GetSSHKeys(ctx context.Context, p *params.SSHKeysRequest) (params.SSHKeysResponse, error) { var r params.SSHKeysResponse err := c.Client.Call(ctx, p, &r) return r, err } // GetUserGroupsWithID returns the groups for a user with the given ID. func (c *client) GetUserGroupsWithID(ctx context.Context, p *params.GetUserGroupsWithIDRequest) (*params.GroupsResponse, error) { var r *params.GroupsResponse err := c.Client.Call(ctx, p, &r) return r, err } // GetUserWithID returns the user information for the request user. func (c *client) GetUserWithID(ctx context.Context, p *params.GetUserWithIDRequest) (*params.User, error) { var r *params.User err := c.Client.Call(ctx, p, &r) return r, err } // ModifyUserGroups updates the groups stored for the given user. Groups // can be either added or removed in a single query. It is an error to // try and both add and remove groups at the same time. func (c *client) ModifyUserGroups(ctx context.Context, p *params.ModifyUserGroupsRequest) error { return c.Client.Call(ctx, p, nil) } // PutSSHKeys updates the set of SSH keys stored for the given user. If // the add parameter is set to true then keys that are already stored // will be added to, otherwise they will be replaced. func (c *client) PutSSHKeys(ctx context.Context, p *params.PutSSHKeysRequest) error { return c.Client.Call(ctx, p, nil) } // QueryUsers filters the user database for users that match the given // request. If no filters are requested all usernames will be returned. func (c *client) QueryUsers(ctx context.Context, p *params.QueryUsersRequest) ([]string, error) { var r []string err := c.Client.Call(ctx, p, &r) return r, err } // SetUserDeprecated creates or updates the user with the given username. If the // user already exists then any IDPGroups or SSHKeys specified in the // request will be ignored. See SetUserGroups, ModifyUserGroups, // SetSSHKeys and DeleteSSHKeys if you wish to manipulate these for a // user. // TODO change this into a create-agent function. func (c *client) SetUserDeprecated(ctx context.Context, p *params.SetUserRequest) error { return c.Client.Call(ctx, p, nil) } // SetUserExtraInfo updates extra-info for the given user. For each // specified extra-info field the stored values will be updated to be the // specified value. All other values will remain unchanged. func (c *client) SetUserExtraInfo(ctx context.Context, p *params.SetUserExtraInfoRequest) error { return c.Client.Call(ctx, p, nil) } // SetUserExtraInfoItem updates the stored extra-info item with the given // key for the given user. func (c *client) SetUserExtraInfoItem(ctx context.Context, p *params.SetUserExtraInfoItemRequest) error { return c.Client.Call(ctx, p, nil) } // SetUserGroups updates the groups stored for the given user to the // given value. func (c *client) SetUserGroups(ctx context.Context, p *params.SetUserGroupsRequest) error { return c.Client.Call(ctx, p, nil) } // User returns the user information for the request user. func (c *client) User(ctx context.Context, p *params.UserRequest) (*params.User, error) { var r *params.User err := c.Client.Call(ctx, p, &r) return r, err } // UserExtraInfo returns any stored extra-info for the given user. func (c *client) UserExtraInfo(ctx context.Context, p *params.UserExtraInfoRequest) (map[string]interface{}, error) { var r map[string]interface{} err := c.Client.Call(ctx, p, &r) return r, err } // UserExtraInfoItem returns any stored extra-info item with the given // key for the given user. func (c *client) UserExtraInfoItem(ctx context.Context, p *params.UserExtraInfoItemRequest) (interface{}, error) { var r interface{} err := c.Client.Call(ctx, p, &r) return r, err } // UserGroups returns the list of groups associated with the requested // user. func (c *client) UserGroups(ctx context.Context, p *params.UserGroupsRequest) ([]string, error) { var r []string err := c.Client.Call(ctx, p, &r) return r, err } // UserIDPGroups returns the list of groups associated with the requested // user. This is deprected and UserGroups should be used in preference. func (c *client) UserIDPGroups(ctx context.Context, p *params.UserIDPGroupsRequest) ([]string, error) { var r []string err := c.Client.Call(ctx, p, &r) return r, err } // UserToken returns a token, in the form of a macaroon, identifying // the user. This token can only be generated by an administrator. func (c *client) UserToken(ctx context.Context, p *params.UserTokenRequest) (*bakery.Macaroon, error) { var r *bakery.Macaroon err := c.Client.Call(ctx, p, &r) return r, err } // VerifyToken verifies that the given token is a macaroon generated by // this service and returns any declared values. func (c *client) VerifyToken(ctx context.Context, p *params.VerifyTokenRequest) (map[string]string, error) { var r map[string]string err := c.Client.Call(ctx, p, &r) return r, err } // WhoAmI returns details of the authenticated user. func (c *client) WhoAmI(ctx context.Context, p *params.WhoAmIRequest) (params.WhoAmIResponse, error) { var r params.WhoAmIResponse err := c.Client.Call(ctx, p, &r) return r, err } golang-github-canonical-candid-1.12.3/candidclient/client_test.go000066400000000000000000000057211457263123000247670ustar00rootroot00000000000000package candidclient_test import ( "context" "sort" "testing" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "gopkg.in/errgo.v1" "github.com/canonical/candid/candidclient" "github.com/canonical/candid/candidtest" ) func TestIdentityClient(t *testing.T) { c := qt.New(t) defer c.Done() srv := candidtest.NewServer() srv.AddUser("bob", "alice", "charlie") testIdentityClient(c, srv.CandidClient("bob"), srv.Client("bob"), "bob", "bob", []string{"alice", "charlie"}, ) } func TestIdentityClientWithDomainStrip(t *testing.T) { c := qt.New(t) defer c.Done() srv := candidtest.NewServer() srv.AddUser("bob@usso", "alice@usso", "charlie@elsewhere") testIdentityClient(c, candidclient.StripDomain(srv.CandidClient("bob@usso"), "usso"), srv.Client("bob@usso"), "bob@usso", "bob", []string{"alice", "charlie@elsewhere"}, ) } func TestIdentityClientWithDomainStripNoDomains(t *testing.T) { c := qt.New(t) defer c.Done() srv := candidtest.NewServer() srv.AddUser("bob", "alice", "charlie") testIdentityClient(c, candidclient.StripDomain(srv.CandidClient("bob"), "usso"), srv.Client("bob"), "bob", "bob", []string{"alice", "charlie"}, ) } // testIdentityClient tests that the given identity client can be used to // create a third party caveat that when discharged provides // an Identity with the given id, user name and groups. func testIdentityClient(c *qt.C, candidClient identchecker.IdentityClient, bclient *httpbakery.Client, expectId, expectUser string, expectGroups []string) { kr := httpbakery.NewThirdPartyLocator(nil, nil) kr.AllowInsecure() b := identchecker.NewBakery(identchecker.BakeryParams{ Locator: kr, Key: bakery.MustGenerateKey(), IdentityClient: candidClient, }) _, authErr := b.Checker.Auth().Allow(context.TODO(), identchecker.LoginOp) derr := errgo.Cause(authErr).(*bakery.DischargeRequiredError) m, err := b.Oven.NewMacaroon(context.TODO(), bakery.LatestVersion, derr.Caveats, derr.Ops...) c.Assert(err, qt.IsNil) ms, err := bclient.DischargeAll(context.TODO(), m) c.Assert(err, qt.IsNil) // Make sure that the macaroon discharged correctly and that it // has the right declared caveats. authInfo, err := b.Checker.Auth(ms).Allow(context.TODO(), identchecker.LoginOp) c.Assert(err, qt.IsNil) c.Assert(authInfo.Identity, qt.Not(qt.IsNil)) c.Assert(authInfo.Identity.Id(), qt.Equals, expectId) c.Assert(authInfo.Identity.Domain(), qt.Equals, "") user := authInfo.Identity.(candidclient.Identity) u, err := user.Username() c.Assert(err, qt.IsNil) c.Assert(u, qt.Equals, expectUser) ok, err := user.Allow(context.TODO(), []string{expectGroups[0]}) c.Assert(err, qt.IsNil) c.Assert(ok, qt.Equals, true) groups, err := user.Groups() c.Assert(err, qt.IsNil) sort.Strings(groups) c.Assert(groups, qt.DeepEquals, expectGroups) } golang-github-canonical-candid-1.12.3/candidclient/groupcache.go000066400000000000000000000037451457263123000245760ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package candidclient import ( "context" "sort" "time" "github.com/juju/utils/v2/cache" "gopkg.in/errgo.v1" "github.com/canonical/candid/params" ) // GroupCache holds a cache of group membership information. type GroupCache struct { cache *cache.Cache client *Client } // NewGroupCache returns a GroupCache that will cache // group membership information. // // It will cache results for at most cacheTime. // // Note that use of this type should be avoided when possible - in // the future it may not be possible to enumerate group membership // for a user. func NewGroupCache(c *Client, cacheTime time.Duration) *GroupCache { return &GroupCache{ cache: cache.New(cacheTime), client: c, } } // Groups returns the set of groups that the user is a member of. func (gc *GroupCache) Groups(username string) ([]string, error) { groupMap, err := gc.groupMap(username) if err != nil { return nil, errgo.Mask(err) } groups := make([]string, 0, len(groupMap)) for g := range groupMap { groups = append(groups, g) } sort.Strings(groups) return groups, nil } func (gc *GroupCache) groupMap(username string) (map[string]bool, error) { groups0, err := gc.cache.Get(username, func() (interface{}, error) { groups, err := gc.client.UserGroups(context.TODO(), ¶ms.UserGroupsRequest{ Username: params.Username(username), }) if err != nil && errgo.Cause(err) != params.ErrNotFound { return nil, errgo.Mask(err) } groupMap := make(map[string]bool) for _, g := range groups { groupMap[g] = true } return groupMap, nil }) if err != nil { return nil, errgo.Notef(err, "cannot fetch groups") } return groups0.(map[string]bool), nil } // CacheEvict evicts username from the cache. func (c *GroupCache) CacheEvict(username string) { c.cache.Evict(username) } // CacheEvictAll evicts everything from the cache. func (c *GroupCache) CacheEvictAll() { c.cache.EvictAll() } golang-github-canonical-candid-1.12.3/candidclient/identity.go000066400000000000000000000062061457263123000243020ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package candidclient import ( "context" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "gopkg.in/errgo.v1" "github.com/canonical/candid/params" ) // Identity represents a Candid identity. It includes bakery.ACLIdentity but // also includes methods for determining the username and // enquiring about groups. // // Note that currently the Id method just returns the user // name, but client code should not rely on it doing that - eventually // it will return an opaque user identifier rather than the user name. type Identity interface { identchecker.ACLIdentity // Username returns the user name of the user. Username() (string, error) // Groups returns all the groups that the user is a member of. // // Note: use of this method should be avoided if possible, as a user may // potentially be in huge numbers of groups. Groups() ([]string, error) } var _ Identity = (*usernameIdentity)(nil) type usernameIdentity struct { client *Client username string } // Username implements Identity.Username. func (id *usernameIdentity) Username() (string, error) { return id.username, nil } // Groups implements Identity.Groups. func (id *usernameIdentity) Groups() ([]string, error) { if id.client.permChecker != nil { return id.client.permChecker.cache.Groups(id.username) } return nil, nil } // Allow implements Identity.Allow. func (id *usernameIdentity) Allow(ctx context.Context, acl []string) (bool, error) { if id.client.permChecker != nil { return id.client.permChecker.Allow(id.username, acl) } // No groups - just implement the trivial cases. ok, _ := trivialAllow(id.username, acl) return ok, nil } // Id implements Identity.Id. func (id *usernameIdentity) Id() string { return id.username } // Domain implements Identity.Domain. func (id *usernameIdentity) Domain() string { return "" } type useridIdentity struct { client *Client user params.User } // Username implements Identity.Username. func (id *useridIdentity) Username() (string, error) { if id.user.Username != "" { return string(id.user.Username), nil } ctx := context.Background() user, err := id.client.GetUserWithID(ctx, ¶ms.GetUserWithIDRequest{ UserID: id.user.ExternalID, }) if err != nil { return "", errgo.Mask(err) } id.user = *user return string(id.user.Username), nil } // Groups implements Identity.Groups. func (id *useridIdentity) Groups() ([]string, error) { _, err := id.Username() if err != nil { return nil, errgo.Mask(err) } return id.user.IDPGroups, nil } // Allow implements Identity.Allow. func (id *useridIdentity) Allow(ctx context.Context, acl []string) (bool, error) { groups, err := id.Groups() if err != nil { return false, errgo.Mask(err) } groups = append(groups, string(id.user.Username)) for _, g := range groups { if ok, _ := trivialAllow(g, acl); ok { return true, nil } } return false, nil } // Id implements Identity.Id. func (id *useridIdentity) Id() string { return id.user.ExternalID } // Domain implements Identity.Domain. func (id *useridIdentity) Domain() string { return "" } golang-github-canonical-candid-1.12.3/candidclient/permcheck.go000066400000000000000000000047221457263123000244130ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package candidclient import ( "strings" "time" "gopkg.in/errgo.v1" ) // TODO unexport this type - it's best exposed as part of the client API only. // PermChecker provides a way to query ACLs using the identity client. type PermChecker struct { cache *GroupCache } // NewPermChecker returns a permission checker // that uses the given identity client to check permissions. // // It will cache results for at most cacheTime. func NewPermChecker(c *Client, cacheTime time.Duration) *PermChecker { return &PermChecker{ cache: NewGroupCache(c, cacheTime), } } // NewPermCheckerWithCache returns a new PermChecker using // the given cache for its group queries. func NewPermCheckerWithCache(cache *GroupCache) *PermChecker { return &PermChecker{ cache: cache, } } // trivialAllow reports whether the username should be allowed // access to the given ACL based on a superficial inspection // of the ACL. If there is a definite answer, it will return // a true isTrivial; otherwise it will return (false, false). func trivialAllow(username string, acl []string) (allow, isTrivial bool) { if len(acl) == 0 { return false, true } for _, name := range acl { if name == username { return true, true } suffix := strings.TrimPrefix(name, "everyone") if len(suffix) == len(name) { continue } if suffix != "" && suffix[0] != '@' { continue } // name is either "everyone" or "everyone@somewhere". We consider // the user to be part of everyone@somewhere if their username has // the suffix @somewhere. if strings.HasSuffix(username, suffix) { return true, true } } return false, false } // Allow reports whether the given ACL admits the user with the given // name. If the user does not exist and the ACL does not allow username // or everyone, it will return (false, nil). func (c *PermChecker) Allow(username string, acl []string) (bool, error) { if ok, isTrivial := trivialAllow(username, acl); isTrivial { return ok, nil } groups, err := c.cache.groupMap(username) if err != nil { return false, errgo.Mask(err) } for _, a := range acl { if groups[a] { return true, nil } } return false, nil } // CacheEvict evicts username from the cache. func (c *PermChecker) CacheEvict(username string) { c.cache.CacheEvict(username) } // CacheEvictAll evicts everything from the cache. func (c *PermChecker) CacheEvictAll() { c.cache.CacheEvictAll() } golang-github-canonical-candid-1.12.3/candidclient/permcheck_test.go000066400000000000000000000064141457263123000254520ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package candidclient_test import ( "testing" "time" qt "github.com/frankban/quicktest" "github.com/canonical/candid/candidclient" "github.com/canonical/candid/candidtest" ) func TestPermChecker(t *testing.T) { c := qt.New(t) defer c.Done() srv := candidtest.NewServer() srv.AddUser("server-user", candidtest.GroupListGroup) srv.AddUser("alice", "somegroup") client, err := candidclient.New(candidclient.NewParams{ BaseURL: srv.URL.String(), Client: srv.Client("server-user"), }) c.Assert(err, qt.IsNil) pc := candidclient.NewPermChecker(client, time.Hour) // No permissions always yields false. ok, err := pc.Allow("bob", nil) c.Assert(err, qt.IsNil) c.Assert(ok, qt.Equals, false) // If the user isn't found, we return a (false, nil) ok, err = pc.Allow("bob", []string{"beatles"}) c.Assert(err, qt.IsNil) c.Assert(ok, qt.Equals, false) // If the perms allow everyone, it's ok ok, err = pc.Allow("bob", []string{"noone", "everyone"}) c.Assert(err, qt.IsNil) c.Assert(ok, qt.Equals, true) // If the perms allow everyone@somewhere, it's ok. ok, err = pc.Allow("bob@somewhere", []string{"everyone@somewhere"}) c.Assert(err, qt.IsNil) c.Assert(ok, qt.Equals, true) // Check that the everyone@x logic works with multiple @s. ok, err = pc.Allow("bob@foo@somewhere@else", []string{"everyone@somewhere@else"}) c.Assert(err, qt.IsNil) c.Assert(ok, qt.Equals, true) // Check that we're careful enough about "everyone" as a prefix // to a user name. ok, err = pc.Allow("bobx", []string{"everyonex"}) c.Assert(err, qt.IsNil) c.Assert(ok, qt.Equals, false) // If the perms allow the user itself, it's ok ok, err = pc.Allow("bob", []string{"noone", "bob"}) c.Assert(err, qt.IsNil) c.Assert(ok, qt.Equals, true) srv.AddUser("bob", "beatles") // The group details are currently cached by the client, // so the original request will still fail. ok, err = pc.Allow("bob", []string{"beatles"}) c.Assert(err, qt.IsNil) c.Assert(ok, qt.Equals, false) // Clearing the cache allows it to succeed. pc.CacheEvictAll() ok, err = pc.Allow("bob", []string{"beatles"}) c.Assert(err, qt.IsNil) c.Assert(ok, qt.Equals, true) } func TestGroupCache(t *testing.T) { c := qt.New(t) defer c.Done() srv := candidtest.NewServer() srv.AddUser("server-user", candidtest.GroupListGroup) srv.AddUser("alice", "somegroup", "othergroup") client, err := candidclient.New(candidclient.NewParams{ BaseURL: srv.URL.String(), Client: srv.Client("server-user"), }) c.Assert(err, qt.IsNil) cache := candidclient.NewGroupCache(client, time.Hour) // If the user isn't found, we retturn no groups. g, err := cache.Groups("bob") c.Assert(err, qt.IsNil) c.Assert(g, qt.HasLen, 0) g, err = cache.Groups("alice") c.Assert(err, qt.IsNil) c.Assert(g, qt.DeepEquals, []string{"othergroup", "somegroup"}) srv.AddUser("bob", "beatles") // The group details are currently cached by the client, // so we'll still see the original group membership. g, err = cache.Groups("bob") c.Assert(err, qt.IsNil) c.Assert(g, qt.HasLen, 0) // Clearing the cache allows it to succeed. cache.CacheEvictAll() g, err = cache.Groups("bob") c.Assert(err, qt.IsNil) c.Assert(g, qt.DeepEquals, []string{"beatles"}) } golang-github-canonical-candid-1.12.3/candidclient/redirect/000077500000000000000000000000001457263123000237175ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/candidclient/redirect/redirect.go000066400000000000000000000104201457263123000260440ustar00rootroot00000000000000// Copyright 2019 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. // Package redirect implements redirection based login. package redirect import ( "context" "net/url" "strings" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" errgo "gopkg.in/errgo.v1" httprequest "gopkg.in/httprequest.v1" "github.com/canonical/candid/params" ) const Kind = "browser-redirect" type InteractionInfo struct { // LoginURL contains the URL to redirect to in order to start a // login attempt. LoginURL string // DischargeTokenURL contains the URL that is used to swap a // login code for a discharge token. DischargeTokenURL string } // RedirectURL calculates the URL to redirect to in order to initate a // browser-redirect login. func (i InteractionInfo) RedirectURL(returnTo, state string) string { v := make(url.Values, 2) v.Set("return_to", returnTo) v.Set("state", state) var sb strings.Builder sb.WriteString(i.LoginURL) if strings.Contains(i.LoginURL, "?") { sb.WriteByte('&') } else { sb.WriteByte('?') } sb.WriteString(v.Encode()) return sb.String() } // SetInteraction adds interaction info the the browser-redirect interaction type. func SetInteraction(ierr *httpbakery.Error, loginURL, dischargeTokenURL string) { ierr.SetInteraction(Kind, InteractionInfo{ LoginURL: loginURL, DischargeTokenURL: dischargeTokenURL, }) } // DischargeTokenRequest represents a request to the DischargeTokenURL. type DischargeTokenRequest struct { httprequest.Route `httprequest:"POST"` Body struct { Code string `json:"code"` } `httprequest:",body"` } // DischargeTokenResponse contains a response from a DischargeTokenURL. type DischargeTokenResponse struct { DischargeToken *httpbakery.DischargeToken `json:"token,omitempty"` } // GetDischargeToken retrieves the discharge token associated with the // given code. func (i InteractionInfo) GetDischargeToken(ctx context.Context, code string) (*httpbakery.DischargeToken, error) { client := new(httprequest.Client) var req DischargeTokenRequest req.Body.Code = code var resp DischargeTokenResponse if err := client.CallURL(ctx, i.DischargeTokenURL, &req, &resp); err != nil { return nil, errgo.Mask(err) } return resp.DischargeToken, nil } // ParseLoginResult extracts the result from a response callback. func ParseLoginResult(requestURL string) (state, code string, err error) { u, err := url.Parse(requestURL) if err != nil { return "", "", errgo.Mask(err) } v := u.Query() if e := v.Get("error"); e != "" { if ec := v.Get("error_code"); ec != "" { return v.Get("state"), "", errgo.WithCausef(nil, params.ErrorCode(ec), "%s", e) } return v.Get("state"), "", errgo.Newf("%s", e) } return v.Get("state"), v.Get("code"), nil } type Interactor struct { dischargeTokens map[string]httpbakery.DischargeToken } // Kind implements httpbakery.Interactor. func (*Interactor) Kind() string { return Kind } // Interact implements httpbakery.Interactor. func (i *Interactor) Interact(ctx context.Context, _ *httpbakery.Client, _ string, ierr *httpbakery.Error) (*httpbakery.DischargeToken, error) { var v InteractionInfo if err := ierr.InteractionMethod(Kind, &v); err != nil { return nil, errgo.Mask(err, errgo.Is(httpbakery.ErrInteractionMethodNotFound)) } if dt, ok := i.dischargeTokens[v.LoginURL]; ok { return &dt, nil } return nil, &httpbakery.InteractionError{ Reason: &RedirectRequiredError{ InteractionInfo: v, }, } } // SetDischargeToken sets a discharge token for a particular login URL. func (i *Interactor) SetDischargeToken(loginURL string, dt *httpbakery.DischargeToken) { if i.dischargeTokens == nil { i.dischargeTokens = make(map[string]httpbakery.DischargeToken) } if dt == nil { delete(i.dischargeTokens, loginURL) } else { i.dischargeTokens[loginURL] = *dt } } // A RedirectRequiredError is the type of error returned from an // interactor when an interaction via redirection is required. type RedirectRequiredError struct { InteractionInfo InteractionInfo } // Error implements error. func (e RedirectRequiredError) Error() string { return "redirect required" } // IsRedirectRequiredError determines if an error is a // RedirectRequiredError. func IsRedirectRequiredError(err error) bool { _, ok := err.(*RedirectRequiredError) return ok } golang-github-canonical-candid-1.12.3/candidclient/redirect/redirect_test.go000066400000000000000000000060521457263123000271110ustar00rootroot00000000000000// Copyright 2019 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package redirect_test import ( "context" "net/http" "testing" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/candidclient/redirect" "github.com/canonical/candid/params" ) func TestRedirectURL(t *testing.T) { c := qt.New(t) info := redirect.InteractionInfo{ LoginURL: "https://www.example.com/login", } rurl := info.RedirectURL("https://www.example.com/callback", "12345") c.Assert(rurl, qt.Equals, "https://www.example.com/login?return_to=https%3A%2F%2Fwww.example.com%2Fcallback&state=12345") info = redirect.InteractionInfo{ LoginURL: "https://www.example.com/login?domain=test", } rurl = info.RedirectURL("https://www.example.com/callback", "12345") c.Assert(rurl, qt.Equals, "https://www.example.com/login?domain=test&return_to=https%3A%2F%2Fwww.example.com%2Fcallback&state=12345") } func TestInteractor(t *testing.T) { c := qt.New(t) ctx := context.Background() var i redirect.Interactor c.Assert(i.Kind(), qt.Equals, redirect.Kind) req, err := http.NewRequest("GET", "https://www.example.com/discharge", nil) c.Assert(err, qt.IsNil) irerr := httpbakery.NewInteractionRequiredError(nil, req) // Fake an empty InteractionRequiredError irerr.Info = &httpbakery.ErrorInfo{} _, err = i.Interact(ctx, nil, "", irerr) c.Assert(errgo.Cause(err), qt.Equals, httpbakery.ErrInteractionMethodNotFound) redirect.SetInteraction(irerr, "https://www.example.com/login", "https://www.example.com/token") _, err = i.Interact(ctx, nil, "", irerr) c.Assert(err, qt.Satisfies, httpbakery.IsInteractionError) ierr := err.(*httpbakery.InteractionError) c.Assert(ierr.Reason, qt.Satisfies, redirect.IsRedirectRequiredError) c.Assert(ierr.Reason.(*redirect.RedirectRequiredError).InteractionInfo, qt.Equals, redirect.InteractionInfo{ LoginURL: "https://www.example.com/login", DischargeTokenURL: "https://www.example.com/token", }) dt := &httpbakery.DischargeToken{ Kind: "test", Value: []byte("test"), } i.SetDischargeToken("https://www.example.com/login", dt) dt2, err := i.Interact(ctx, nil, "", irerr) c.Assert(err, qt.IsNil) c.Assert(*dt2, qt.DeepEquals, *dt) } func TestParseLoginResult(t *testing.T) { c := qt.New(t) state, code, err := redirect.ParseLoginResult("https://example.com/callback?state=12345&code=54321") c.Assert(state, qt.Equals, "12345") c.Assert(err, qt.IsNil) c.Assert(code, qt.Equals, "54321") state, code, err = redirect.ParseLoginResult("https://example.com/callback?state=12345&error_code=ec&error=test+error") c.Assert(state, qt.Equals, "12345") c.Assert(errgo.Cause(err), qt.Equals, params.ErrorCode("ec")) c.Assert(err, qt.ErrorMatches, "test error") c.Assert(code, qt.Equals, "") state, code, err = redirect.ParseLoginResult("https://example.com/callback?state=12345&error=test+error") c.Assert(state, qt.Equals, "12345") c.Assert(err, qt.ErrorMatches, "test error") c.Assert(code, qt.Equals, "") } golang-github-canonical-candid-1.12.3/candidclient/strip.go000066400000000000000000000061641457263123000236150ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package candidclient import ( "context" "strings" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "gopkg.in/errgo.v1" ) // StripDomain returns an implementation of identchecker.IdentityClient // that strips the given // domain name off any user and group names returned from it. It also // adds it as an @ suffix when querying for ACL membership for names // that don't already contain a domain. // // This is useful when an existing user of the identity manager needs to // obtain backwardly compatible usernames when an identity manager is // changed to add a domain suffix. func StripDomain(candidClient *Client, domain string) identchecker.IdentityClient { return &domainStrippingClient{ domain: "@" + domain, c: candidClient, } } // domainStrippingClient implements IdentityClient by stripping a given // domain off any declared users. type domainStrippingClient struct { domain string c *Client } // DeclaredIdentity implements IdentityClient.DeclaredIdentity. func (c *domainStrippingClient) DeclaredIdentity(ctx context.Context, attrs map[string]string) (identchecker.Identity, error) { ident0, err := c.c.DeclaredIdentity(ctx, attrs) if err != nil { return nil, err } return &domainStrippingIdentity{ Identity: ident0.(Identity), domain: c.domain, }, nil } // DeclaredIdentity implements IdentityClient.IdentityCaveats. func (c *domainStrippingClient) IdentityFromContext(ctx context.Context) (identchecker.Identity, []checkers.Caveat, error) { return c.c.IdentityFromContext(ctx) } var _ Identity = (*domainStrippingIdentity)(nil) type domainStrippingIdentity struct { domain string Identity } // Username implements ACLUser.IdentityCaveats. func (u *domainStrippingIdentity) Username() (string, error) { name, err := u.Identity.Username() if err != nil { return "", err } return strings.TrimSuffix(name, u.domain), nil } // Groups implements ACLUser.Groups. func (u *domainStrippingIdentity) Groups() ([]string, error) { groups, err := u.Identity.Groups() if err != nil { return nil, err } for i, g := range groups { groups[i] = strings.TrimSuffix(g, u.domain) } return groups, nil } // Allow implements ACLUser.Allow by adding stripped // domain to all names in acl that don't have a domain // before calling the underlying Allow method. func (u *domainStrippingIdentity) Allow(ctx context.Context, acl []string) (bool, error) { acl1 := make([]string, len(acl)) for i, name := range acl { if !strings.Contains(name, "@") { acl1[i] = name + u.domain } else { acl1[i] = name } } ok, err := u.Identity.Allow(ctx, acl1) if err != nil { return false, errgo.Mask(err) } if ok { return true, nil } // We were denied access with the suffix added, but perhaps // the identity manager isn't yet adding suffixes - we still // want it to work in that case, so try without the added // suffixes. ok, err = u.Identity.Allow(ctx, acl) if err != nil { return false, errgo.Mask(err) } return ok, nil } golang-github-canonical-candid-1.12.3/candidclient/ussodischarge/000077500000000000000000000000001457263123000247615ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/candidclient/ussodischarge/client.go000066400000000000000000000107071457263123000265730ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. // Package ussomacaroon provides a client that can authenticate with an // identity server by discharging macaroons on an Ubuntu SSO server. package ussodischarge import ( "context" stdurl "net/url" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" errgo "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "gopkg.in/macaroon.v2" ) const protocolName = "usso_macaroon" // Macaroon returns a macaroon from the identity provider at the given // URL that can be discharged using a Discharger. If doer is non-nil // then it will be used to collect the macaroon. func Macaroon(ctx context.Context, doer httprequest.Doer, url string) (*bakery.Macaroon, error) { client := &httprequest.Client{ Doer: doer, } var resp MacaroonResponse if err := client.Get(ctx, url, &resp); err != nil { return nil, errgo.Notef(err, "cannot get macaroon") } return resp.Macaroon, nil } type interactionInfo struct { URL string `json:"url"` } func SetInteraction(ierr *httpbakery.Error, url string) { ierr.SetInteraction(protocolName, interactionInfo{URL: url}) } // Interactor is an httpbakery.Interactor that will login using a // macaroon discharged by an Ubuntu SSO service. type Interactor struct { f func(*httpbakery.Client, string) (macaroon.Slice, error) } // NewInteractor creates an Interactor which uses a macaroon previously // collected with Macaroon and discharged by the requisit Ubuntu SSO // service to log in. The discharged macaroon to use will be requested // from the given function when required. func NewInteractor(f func(client *httpbakery.Client, url string) (macaroon.Slice, error)) *Interactor { return &Interactor{ f: f, } } func (i *Interactor) Kind() string { return protocolName } func (i *Interactor) Interact(ctx context.Context, client *httpbakery.Client, location string, ierr *httpbakery.Error) (*httpbakery.DischargeToken, error) { var info interactionInfo if err := ierr.InteractionMethod(protocolName, &info); err != nil { return nil, errgo.Mask(err, errgo.Is(httpbakery.ErrInteractionMethodNotFound)) } ms, err := i.f(client, info.URL) if err != nil { return nil, errgo.Mask(err, errgo.Any) } cl := httprequest.Client{ Doer: client, } var resp LoginResponse err = cl.CallURL(ctx, info.URL, &LoginRequest{ Login: Login{ Macaroons: ms, }, }, &resp) return resp.DischargeToken, errgo.Mask(err) } // LegacyInteract implements httpbakery.LegacyInteractor // for the Interactor. func (i *Interactor) LegacyInteract(ctx context.Context, client *httpbakery.Client, location string, visitURL *stdurl.URL) error { ms, err := i.f(client, visitURL.String()) if err != nil { return errgo.Mask(err, errgo.Any) } cl := httprequest.Client{ Doer: client, } err = cl.CallURL(ctx, visitURL.String(), &LoginRequest{ Login: Login{ Macaroons: ms, }, }, nil) return errgo.Mask(err) } // Discharger is a client that can discharge Ubuntu SSO third-party // caveats. type Discharger struct { // Email contains the email address of the user. Email string // Password contains the password of the user. Password string // OTP contains the verification code of the user. OTP string // Doer will be used to perform the discharge if non-nil. Doer httprequest.Doer } // AcquireDischarge discharges the given Ubuntu SSO third-party caveat using the // user information from the Discharger. func (d *Discharger) AcquireDischarge(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { if len(payload) > 0 { return nil, errgo.Newf("USSO does not support macaroon-external third party caveats") } client := httprequest.Client{ BaseURL: cav.Location, Doer: d.Doer, } req := &ussoDischargeRequest{ Discharge: ussoDischarge{ Email: d.Email, Password: d.Password, OTP: d.OTP, CaveatID: string(cav.Id), }, } var resp ussoDischargeResponse if err := client.Call(ctx, req, &resp); err != nil { return nil, errgo.Mask(err) } return bakery.NewLegacyMacaroon(&resp.Macaroon.Macaroon) } // DischargeAll discharges the given macaroon which is assumed to only // have third-party caveats addressed to an Ubuntu SSO server. func (d *Discharger) DischargeAll(ctx context.Context, m *bakery.Macaroon) (macaroon.Slice, error) { ms, err := bakery.DischargeAll(ctx, m, d.AcquireDischarge) if err != nil { return nil, errgo.Mask(err) } return ms, nil } golang-github-canonical-candid-1.12.3/candidclient/ussodischarge/client_test.go000066400000000000000000000214471457263123000276350ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package ussodischarge_test import ( "context" "net/http" "net/http/httptest" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" errgo "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "gopkg.in/macaroon.v2" "github.com/canonical/candid/candidclient/ussodischarge" "github.com/canonical/candid/params" ) var _ httpbakery.Interactor = (*ussodischarge.Interactor)(nil) var _ httpbakery.LegacyInteractor = (*ussodischarge.Interactor)(nil) var testContext = context.Background() var macaroonEquals = qt.CmpEquals(cmp.AllowUnexported(macaroon.Macaroon{}), cmpopts.EquateEmpty()) func TestClient(t *testing.T) { c := qt.New(t) defer c.Done() qtsuite.Run(c, &clientSuite{}) } type clientSuite struct { testMacaroon *bakery.Macaroon testDischargeMacaroon *macaroon.Macaroon srv *httptest.Server // macaroon is returned from the /macaroon endpoint of the test server. // If this is nil, an error will be returned instead. macaroon *bakery.Macaroon } // ServeHTTP allows us to use the test suite as a handler to test the // client methods against. func (s *clientSuite) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/macaroon": s.serveMacaroon(w, r) case "/login": s.serveLogin(w, r) case "/api/v2/tokens/discharge": s.serveDischarge(w, r) default: http.NotFound(w, r) } } func (s *clientSuite) Init(c *qt.C) { var err error s.testMacaroon, err = bakery.NewMacaroon([]byte("test rootkey"), []byte("test macaroon"), "test location", bakery.LatestVersion, nil) c.Assert(err, qt.IsNil) // Discharge macaroons from Ubuntu SSO will be binary encoded in the version 1 format. s.testDischargeMacaroon, err = macaroon.New([]byte("test discharge rootkey"), []byte("test discharge macaroon"), "test discharge location", macaroon.V1) c.Assert(err, qt.IsNil) s.srv = httptest.NewServer(s) c.Defer(s.srv.Close) s.macaroon = nil } func (s *clientSuite) TestMacaroon(c *qt.C) { s.macaroon = s.testMacaroon m, err := ussodischarge.Macaroon(testContext, nil, s.srv.URL+"/macaroon") c.Assert(err, qt.IsNil) c.Assert(m.M(), macaroonEquals, s.testMacaroon.M()) } func (s *clientSuite) TestMacaroonError(c *qt.C) { m, err := ussodischarge.Macaroon(testContext, nil, s.srv.URL+"/macaroon") c.Assert(m, qt.IsNil) c.Assert(err, qt.ErrorMatches, `cannot get macaroon: Get http.*: test error`) } func (s *clientSuite) TestVisitor(c *qt.C) { v := ussodischarge.NewInteractor(func(_ *httpbakery.Client, url string) (macaroon.Slice, error) { c.Assert(url, qt.Equals, s.srv.URL+"/login") return macaroon.Slice{s.testMacaroon.M()}, nil }) client := httpbakery.NewClient() req, err := http.NewRequest("GET", "", nil) c.Assert(err, qt.IsNil) ierr := httpbakery.NewInteractionRequiredError(nil, req) ussodischarge.SetInteraction(ierr, s.srv.URL+"/login") dt, err := v.Interact(testContext, client, "", ierr) c.Assert(err, qt.IsNil) c.Assert(dt, qt.DeepEquals, &httpbakery.DischargeToken{ Kind: "test-kind", Value: []byte("test-value"), }) } func (s *clientSuite) TestVisitorMethodNotSupported(c *qt.C) { v := ussodischarge.NewInteractor(func(_ *httpbakery.Client, url string) (macaroon.Slice, error) { return nil, errgo.New("function called unexpectedly") }) client := httpbakery.NewClient() req, err := http.NewRequest("GET", "", nil) c.Assert(err, qt.IsNil) ierr := httpbakery.NewInteractionRequiredError(nil, req) ierr.SetInteraction("other", nil) dt, err := v.Interact(testContext, client, "", ierr) c.Assert(errgo.Cause(err), qt.Equals, httpbakery.ErrInteractionMethodNotFound) c.Assert(dt, qt.IsNil) } func (s *clientSuite) TestVisitorFunctionError(c *qt.C) { v := ussodischarge.NewInteractor(func(_ *httpbakery.Client, url string) (macaroon.Slice, error) { return nil, errgo.WithCausef(nil, testCause, "test error") }) client := httpbakery.NewClient() req, err := http.NewRequest("GET", "", nil) c.Assert(err, qt.IsNil) ierr := httpbakery.NewInteractionRequiredError(nil, req) ussodischarge.SetInteraction(ierr, s.srv.URL+"/login") dt, err := v.Interact(testContext, client, "", ierr) c.Assert(errgo.Cause(err), qt.Equals, testCause) c.Assert(err, qt.ErrorMatches, "test error") c.Assert(dt, qt.IsNil) } func (s *clientSuite) TestAcquireDischarge(c *qt.C) { d := &ussodischarge.Discharger{ Email: "user@example.com", Password: "secret", OTP: "123456", } m, err := d.AcquireDischarge(testContext, macaroon.Caveat{ Location: s.srv.URL, Id: []byte("test caveat id"), }, nil) c.Assert(err, qt.IsNil) c.Assert(m.M(), macaroonEquals, s.testDischargeMacaroon) } func (s *clientSuite) TestAcquireDischargeError(c *qt.C) { d := &ussodischarge.Discharger{ Email: "user@example.com", Password: "bad-secret", OTP: "123456", } m, err := d.AcquireDischarge(testContext, macaroon.Caveat{ Location: s.srv.URL, Id: []byte("test caveat id"), }, nil) c.Assert(err, qt.ErrorMatches, `Post http.*: Provided email/password is not correct.`) c.Assert(m, qt.IsNil) } func (s *clientSuite) TestDischargeAll(c *qt.C) { m := s.testMacaroon.Clone() err := m.M().AddThirdPartyCaveat([]byte("third party root key"), []byte("third party caveat id"), s.srv.URL) c.Assert(err, qt.IsNil) d := &ussodischarge.Discharger{ Email: "user@example.com", Password: "secret", OTP: "123456", } ms, err := d.DischargeAll(testContext, m) c.Assert(err, qt.IsNil) md := s.testDischargeMacaroon.Clone() md.Bind(m.M().Signature()) c.Assert(ms, macaroonEquals, macaroon.Slice{m.M(), md}) } func (s *clientSuite) TestDischargeAllError(c *qt.C) { m := s.testMacaroon.Clone() err := m.M().AddThirdPartyCaveat([]byte("third party root key"), []byte("third party caveat id"), s.srv.URL) c.Assert(err, qt.IsNil) d := &ussodischarge.Discharger{ Email: "user@example.com", Password: "bad-secret", OTP: "123456", } ms, err := d.DischargeAll(testContext, m) c.Assert(err, qt.ErrorMatches, `cannot get discharge from ".*": Post http.*: Provided email/password is not correct.`) c.Assert(ms, qt.IsNil) } func (s *clientSuite) serveMacaroon(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { fail(w, r, errgo.Newf("bad method: %s", r.Method)) } if s.macaroon != nil { httprequest.WriteJSON(w, http.StatusOK, ussodischarge.MacaroonResponse{ Macaroon: s.macaroon, }) } else { httprequest.WriteJSON(w, http.StatusInternalServerError, params.Error{ Message: "test error", }) } } func (s *clientSuite) serveLogin(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { fail(w, r, errgo.Newf("bad method: %s", r.Method)) } var lr ussodischarge.LoginRequest if err := httprequest.Unmarshal(httprequest.Params{Request: r, Response: w}, &lr); err != nil { fail(w, r, err) } if n := len(lr.Login.Macaroons); n != 1 { fail(w, r, errgo.Newf("macaroon slice has unexpected length %d", n)) } if id := lr.Login.Macaroons[0].Id(); string(id) != "test macaroon" { fail(w, r, errgo.Newf("unexpected macaroon sent %q", string(id))) } httprequest.WriteJSON(w, http.StatusOK, ussodischarge.LoginResponse{ DischargeToken: &httpbakery.DischargeToken{ Kind: "test-kind", Value: []byte("test-value"), }, }) } func (s *clientSuite) serveDischarge(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { fail(w, r, errgo.Newf("bad method: %s", r.Method)) } var dr ussodischarge.USSODischargeRequest if err := httprequest.Unmarshal(httprequest.Params{Request: r, Response: w}, &dr); err != nil { fail(w, r, err) } if dr.Discharge.Email == "" { fail(w, r, errgo.New("email not specified")) } if dr.Discharge.Password == "" { fail(w, r, errgo.New("password not specified")) } if dr.Discharge.OTP == "" { fail(w, r, errgo.New("otp not specified")) } if dr.Discharge.CaveatID == "" { fail(w, r, errgo.New("caveat_id not specified")) } if dr.Discharge.Email != "user@example.com" || dr.Discharge.Password != "secret" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"error_list": [{"message": "Provided email/password is not correct.", "code": "invalid-credentials"}], "message": "Provided email/password is not correct.", "code": "INVALID_CREDENTIALS", "extra": {}}`)) return } var m ussodischarge.USSOMacaroon m.Macaroon = *s.testDischargeMacaroon httprequest.WriteJSON(w, http.StatusOK, map[string]interface{}{"discharge_macaroon": &m}) } func fail(w http.ResponseWriter, r *http.Request, err error) { httprequest.WriteJSON(w, http.StatusBadRequest, params.Error{ Message: err.Error(), }) } var testCause = errgo.New("test cause") golang-github-canonical-candid-1.12.3/candidclient/ussodischarge/export_test.go000066400000000000000000000003721457263123000276720ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package ussodischarge const ProtocolName = protocolName type USSOMacaroon struct { ussoMacaroon } type USSODischargeRequest ussoDischargeRequest golang-github-canonical-candid-1.12.3/candidclient/ussodischarge/params.go000066400000000000000000000047771457263123000266120ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package ussodischarge import ( "encoding/base64" "encoding/json" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" errgo "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "gopkg.in/macaroon.v2" ) // MacaroonResponse is the response from a GET to a usso-macaroon // identity provider, it will be a macaroon with a third party discharge // addressed to an Ubuntu SSO service. type MacaroonResponse struct { Macaroon *bakery.Macaroon `json:"macaroon,omitempty"` } // LoginRequest is a request to log in using a macaroon that has been // discharged by an Ubuntu SSO service. type LoginRequest struct { httprequest.Route `httprequest:"POST"` Login Login `httprequest:",body"` } // Login is the body of a LoginRequest. type Login struct { Macaroons macaroon.Slice `json:"macaroons,omitempty"` } // LoginResponse is the response to a LoginReuest. type LoginResponse struct { DischargeToken *httpbakery.DischargeToken `json:"discharge-token"` } // ussoDischargeRequest is the request to Ubuntu SSO to discharge a // caveat on behalf of a user. type ussoDischargeRequest struct { httprequest.Route `httprequest:"POST /api/v2/tokens/discharge"` Discharge ussoDischarge `httprequest:",body"` } // ussoDischarge is the body of a ussoDischargeRequest. type ussoDischarge struct { Email string `json:"email"` Password string `json:"password"` OTP string `json:"otp,omitempty"` CaveatID string `json:"caveat_id"` } // ussoDischargeResponse is the response from a ussoDischargeRequest type ussoDischargeResponse struct { Macaroon ussoMacaroon `json:"discharge_macaroon"` } type ussoMacaroon struct { macaroon.Macaroon } func (m *ussoMacaroon) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { return errgo.Notef(err, "cannot unmarshal macaroon") } b, err := base64.RawURLEncoding.DecodeString(s) if err != nil { return errgo.Notef(err, "cannot unmarshal macaroon") } if err := m.Macaroon.UnmarshalBinary(b); err != nil { return errgo.Notef(err, "cannot unmarshal macaroon") } return nil } func (m *ussoMacaroon) MarshalJSON() ([]byte, error) { data, err := m.Macaroon.MarshalBinary() if err != nil { return nil, errgo.Mask(err) } s := base64.RawURLEncoding.EncodeToString(data) bytes, err := json.Marshal(s) if err != nil { return nil, errgo.Mask(err) } return bytes, nil } golang-github-canonical-candid-1.12.3/candidclient/ussodischarge/params_test.go000066400000000000000000000034131457263123000276330ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package ussodischarge_test import ( "encoding/json" "testing" qt "github.com/frankban/quicktest" "github.com/canonical/candid/candidclient/ussodischarge" ) func TestUnmarshalUSSOMacaroon(t *testing.T) { c := qt.New(t) defer c.Done() data := []byte(`"MDAxYmxvY2F0aW9uIHRlc3QgbG9jYXRpb24KMDAxZGlkZW50aWZpZXIgdGVzdCBtYWNhcm9vbgowMDJmc2lnbmF0dXJlICaaplwsJeHwPuBK6er_d3DnEnSJ2b85-V9SXsiL6xWOCg"`) var m ussodischarge.USSOMacaroon err := json.Unmarshal(data, &m) c.Assert(err, qt.IsNil) c.Assert(string(m.Macaroon.Id()), qt.Equals, "test macaroon") } func TestUnmarshalUSSOMacaroonNotJSONString(t *testing.T) { c := qt.New(t) defer c.Done() data := []byte(`123`) var m ussodischarge.USSOMacaroon err := json.Unmarshal(data, &m) c.Assert(err, qt.ErrorMatches, `cannot unmarshal macaroon: json: cannot unmarshal number into Go value of type string`) } func TestUnmarshalUSSOMacaroonBadBase64(t *testing.T) { c := qt.New(t) defer c.Done() data := []byte(`"MDAxYmxvY2F0aW9uIHRlc3QgbG9jYXRpb24KMDAxZGlkZW50aWZpZXIgdGVzdCBtYWNhcm9vbgowMDJmc2lnbmF0dXJlICaaplwsJeHwPuBK6er/d3DnEnSJ2b85+V9SXsiL6xWOCg"`) var m ussodischarge.USSOMacaroon err := json.Unmarshal(data, &m) c.Assert(err, qt.ErrorMatches, `cannot unmarshal macaroon: illegal base64 data at input byte 111`) } func TestUnmarshalUSSOMacaroonBadBinary(t *testing.T) { c := qt.New(t) defer c.Done() data := []byte(`"NDAxYmxvY2F0aW9uIHRlc3QgbG9jYXRpb24KMDAxZGlkZW50aWZpZXIgdGVzdCBtYWNhcm9vbgowMDJmc2lnbmF0dXJlICaaplwsJeHwPuBK6er_d3DnEnSJ2b85-V9SXsiL6xWOCg"`) var m ussodischarge.USSOMacaroon err := json.Unmarshal(data, &m) c.Assert(err, qt.ErrorMatches, `cannot unmarshal macaroon: unmarshal v1: packet size too big`) } golang-github-canonical-candid-1.12.3/candidclient/ussologin/000077500000000000000000000000001457263123000241405ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/candidclient/ussologin/export_test.go000066400000000000000000000003131457263123000270440ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package ussologin var ( Server = &server UserKey = userKey PassKey = passKey OTPKey = otpKey ) golang-github-canonical-candid-1.12.3/candidclient/ussologin/ussologin.go000066400000000000000000000124571457263123000265220ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. // Package ussologin defines functionality used for allowing clients // to authenticate with the Candid server using USSO OAuth. package ussologin import ( "context" "encoding/json" "io/ioutil" "os" "path/filepath" "github.com/juju/usso" "gopkg.in/errgo.v1" "gopkg.in/juju/environschema.v1" "gopkg.in/juju/environschema.v1/form" ) type tokenGetter interface { GetTokenWithOTP(username, password, otp, tokenName string) (*usso.SSOData, error) } // This is defined here to allow it to be stubbed out in tests var server tokenGetter = usso.ProductionUbuntuSSOServer var ( userKey = "E-Mail" passKey = "Password" otpKey = "Two-factor auth (Enter for none)" ) // A FormTokenGetter is a TokenGetter implementation that presents a form // to the user to get login details, and then uses those to get a token // from Ubuntu SSO. type FormTokenGetter struct { Filler form.Filler Name string } // GetToken uses filler to interact with the user and uses the provided // information to obtain an OAuth token from Ubuntu SSO. The returned // token can subsequently be used with LoginWithToken to perform a login. // The tokenName argument is used as the name of the generated token in // Ubuntu SSO. If Ubuntu SSO returned an error when trying to retrieve // the token the error will have a cause of type *usso.Error. func (g FormTokenGetter) GetToken(ctx context.Context) (*usso.SSOData, error) { if g.Name == "" { g.Name = "candidclient" } login, err := g.Filler.Fill(loginForm) if err != nil { return nil, errgo.Notef(err, "cannot read login parameters") } tok, err := server.GetTokenWithOTP( login[userKey].(string), login[passKey].(string), login[otpKey].(string), g.Name, ) if err != nil { return nil, errgo.NoteMask(err, "cannot get token", isUSSOError) } return tok, nil } // loginForm contains the fields required for login. var loginForm = form.Form{ Title: "Login to Ubuntu SSO", Fields: environschema.Fields{ userKey: environschema.Attr{ Description: "Username", Type: environschema.Tstring, Mandatory: true, Group: "1", }, passKey: environschema.Attr{ Description: "Password", Type: environschema.Tstring, Mandatory: true, Secret: true, Group: "1", }, otpKey: environschema.Attr{ Description: "Two-factor auth", Type: environschema.Tstring, Mandatory: true, Group: "2", }, }, } // A TokenGetter is used to fetch a Ubuntu SSO OAuth token. type TokenGetter interface { GetToken(context.Context) (*usso.SSOData, error) } // A StoreTokenGetter is a TokenGetter that will try to retrieve the // token from some storage, before falling back to another TokenGetter. // If the fallback TokenGetter sucessfully retrieves a token then that // token will be put in the store. type StoreTokenGetter struct { Store TokenStore TokenGetter TokenGetter } // GetToken implements TokenGetter.GetToken. A token is first attmepted // to retireve from the store. If a stored token is not available then // GetToken will fallback to TokenGetter.GetToken (if configured). func (g StoreTokenGetter) GetToken(ctx context.Context) (*usso.SSOData, error) { tok, err := g.Store.Get() if err == nil { return tok, nil } if g.TokenGetter == nil { return nil, errgo.Mask(err, errgo.Any) } tok, err = g.TokenGetter.GetToken(ctx) if err == nil { // Ignore any errors storing the token, the user will // just have to get it again next time. g.Store.Put(tok) } return tok, errgo.Mask(err, errgo.Any) } // TokenStore defines the interface for something that can store and // returns oauth tokens. type TokenStore interface { // Put stores an Ubuntu SSO OAuth token. Put(tok *usso.SSOData) error // Get returns an Ubuntu SSO OAuth token from store Get() (*usso.SSOData, error) } // FileTokenStore implements the TokenStore interface by storing the // JSON-encoded oauth token in a file. type FileTokenStore struct { path string } // NewFileTokenStore returns a new FileTokenStore // that uses the given path for storage. func NewFileTokenStore(path string) *FileTokenStore { return &FileTokenStore{path} } // Put implements TokenStore.Put by writing the token to the // FileTokenStore's file. If the file doesn't exist it will be created, // including any required directories. func (f *FileTokenStore) Put(tok *usso.SSOData) error { data, err := json.Marshal(tok) if err != nil { return errgo.Notef(err, "cannot marshal token") } dir := filepath.Dir(f.path) if err := os.MkdirAll(dir, 0700); err != nil { return errgo.Notef(err, "cannot create directory %q", dir) } if err := ioutil.WriteFile(f.path, data, 0600); err != nil { return errgo.Notef(err, "cannot write file") } return nil } // Get implements TokenStore.Get by // reading the token from the FileTokenStore's file. func (f *FileTokenStore) Get() (*usso.SSOData, error) { data, err := ioutil.ReadFile(f.path) if err != nil { return nil, errgo.Notef(err, "cannot read token") } var tok usso.SSOData if err := json.Unmarshal(data, &tok); err != nil { return nil, errgo.Notef(err, "cannot unmarshal token") } return &tok, nil } // isUSSOError determines if err represents an error of type *usso.Error. func isUSSOError(err error) bool { _, ok := err.(*usso.Error) return ok } golang-github-canonical-candid-1.12.3/candidclient/ussologin/ussologin_test.go000066400000000000000000000127661457263123000275640ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package ussologin_test import ( "context" "encoding/json" "fmt" "io/ioutil" "path/filepath" "testing" qt "github.com/frankban/quicktest" jt "github.com/juju/testing" "github.com/juju/usso" errgo "gopkg.in/errgo.v1" "gopkg.in/juju/environschema.v1/form" "github.com/canonical/candid/candidclient/ussologin" ) func TestPutGetToken(t *testing.T) { c := qt.New(t) defer c.Done() token := &usso.SSOData{ ConsumerKey: "consumerkey", ConsumerSecret: "consumersecret", Realm: "realm", TokenKey: "tokenkey", TokenName: "tokenname", TokenSecret: "tokensecret", } path := filepath.Join(c.Mkdir(), "subdir", "tokenFile") store := ussologin.NewFileTokenStore(path) err := store.Put(token) c.Assert(err, qt.IsNil) tok, err := store.Get() c.Assert(err, qt.IsNil) c.Assert(tok, qt.DeepEquals, token) data, err := ioutil.ReadFile(path) c.Assert(err, qt.IsNil) var storedToken *usso.SSOData err = json.Unmarshal(data, &storedToken) c.Assert(err, qt.IsNil) c.Assert(token, qt.DeepEquals, storedToken) } func TestReadInvalidToken(t *testing.T) { c := qt.New(t) defer c.Done() path := fmt.Sprintf("%s/tokenFile", c.Mkdir()) err := ioutil.WriteFile(path, []byte("foobar"), 0700) c.Assert(err, qt.IsNil) store := ussologin.NewFileTokenStore(path) _, err = store.Get() c.Assert(err, qt.ErrorMatches, `cannot unmarshal token: invalid character 'o' in literal false \(expecting 'a'\)`) } func TestTokenInStore(t *testing.T) { c := qt.New(t) defer c.Done() testToken := &usso.SSOData{ ConsumerKey: "consumerkey", ConsumerSecret: "consumersecret", Realm: "realm", TokenKey: "tokenkey", TokenName: "tokenname", TokenSecret: "tokensecret", } st := &testTokenStore{ tok: testToken, } g := &ussologin.StoreTokenGetter{ Store: st, } ctx := context.Background() tok, err := g.GetToken(ctx) c.Assert(err, qt.IsNil) c.Assert(tok, qt.DeepEquals, testToken) c.Assert(st.Calls(), qt.DeepEquals, []jt.StubCall{{ FuncName: "Get", }}) } func TestTokenNotInStore(t *testing.T) { c := qt.New(t) defer c.Done() testToken := &usso.SSOData{ ConsumerKey: "consumerkey", ConsumerSecret: "consumersecret", Realm: "realm", TokenKey: "tokenkey", TokenName: "tokenname", TokenSecret: "tokensecret", } st := &testTokenStore{} st.SetErrors(errgo.New("not found")) fg := &testTokenGetter{ tok: testToken, } g := &ussologin.StoreTokenGetter{ Store: st, TokenGetter: fg, } ctx := context.Background() tok, err := g.GetToken(ctx) c.Assert(err, qt.IsNil) c.Assert(tok, qt.DeepEquals, testToken) c.Assert(st.Calls(), qt.DeepEquals, []jt.StubCall{{ FuncName: "Get", }, { FuncName: "Put", Args: []interface{}{testToken}, }}) c.Assert(fg.Calls(), qt.DeepEquals, []jt.StubCall{{ FuncName: "GetToken", Args: []interface{}{ctx}, }}) } func TestCorrectUserPasswordSentToUSSOServer(t *testing.T) { c := qt.New(t) defer c.Done() ussoStub := &ussoServerStub{} c.Patch(ussologin.Server, ussoStub) tg := ussologin.FormTokenGetter{ Filler: &testFiller{ map[string]interface{}{ ussologin.UserKey: "foobar", ussologin.PassKey: "pass", ussologin.OTPKey: "1234", }}, Name: "testToken", } _, err := tg.GetToken(context.Background()) c.Assert(err, qt.IsNil) calls := ussoStub.Calls() c.Assert(len(calls) > 0, qt.Equals, true) c.Assert(calls[0], qt.DeepEquals, jt.StubCall{ FuncName: "GetTokenWithOTP", Args: []interface{}{"foobar", "pass", "1234", "testToken"}, }) } func TestLoginFailsToGetToken(t *testing.T) { c := qt.New(t) defer c.Done() ussoStub := &ussoServerStub{} ussoStub.SetErrors(errgo.New("something failed")) c.Patch(ussologin.Server, ussoStub) tg := ussologin.FormTokenGetter{ Filler: &testFiller{ map[string]interface{}{ ussologin.UserKey: "foobar", ussologin.PassKey: "pass", ussologin.OTPKey: "1234", }}, Name: "testToken", } _, err := tg.GetToken(context.Background()) c.Assert(err, qt.ErrorMatches, "cannot get token: something failed") } func TestFailedToReadLoginParameters(t *testing.T) { c := qt.New(t) defer c.Done() ussoStub := &ussoServerStub{} c.Patch(ussologin.Server, ussoStub) tg := ussologin.FormTokenGetter{ Filler: &errFiller{}, } _, err := tg.GetToken(context.Background()) c.Assert(err, qt.ErrorMatches, "cannot read login parameters: something failed") c.Assert(ussoStub.Calls(), qt.HasLen, 0) } type testFiller struct { form map[string]interface{} } func (t *testFiller) Fill(f form.Form) (map[string]interface{}, error) { return t.form, nil } type errFiller struct{} func (t *errFiller) Fill(f form.Form) (map[string]interface{}, error) { return nil, errgo.New("something failed") } type ussoServerStub struct { jt.Stub } func (u *ussoServerStub) GetTokenWithOTP(email, password, otp, tokenName string) (*usso.SSOData, error) { u.AddCall("GetTokenWithOTP", email, password, otp, tokenName) return &usso.SSOData{}, u.NextErr() } type testTokenGetter struct { jt.Stub tok *usso.SSOData } func (g *testTokenGetter) GetToken(ctx context.Context) (*usso.SSOData, error) { g.MethodCall(g, "GetToken", ctx) return g.tok, g.NextErr() } type testTokenStore struct { jt.Stub tok *usso.SSOData } func (m *testTokenStore) Put(tok *usso.SSOData) error { m.MethodCall(m, "Put", tok) m.tok = tok return m.NextErr() } func (m *testTokenStore) Get() (*usso.SSOData, error) { m.MethodCall(m, "Get") return m.tok, m.NextErr() } golang-github-canonical-candid-1.12.3/candidclient/ussologin/visitwebpage.go000066400000000000000000000054421457263123000271650ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package ussologin import ( "context" "net/http" "net/url" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/usso" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" ) const interactionMethod = "usso_oauth" type interactionInfo struct { URL string `json:"url,omitempty"` } // SetInteraction sets the required values for the usso_oauth interaction // method on an interaction required error. func SetInteraction(ierr *httpbakery.Error, url string) { ierr.SetInteraction(interactionMethod, interactionInfo{ URL: url, }) } // NewInteractor creates a new httpbakery.Interactor that interacts using // the usso_oauth protocol. func NewInteractor(tg TokenGetter) httpbakery.Interactor { return &interactor{ tg: tg, } } type interactor struct { tg TokenGetter } // Kind implements httpbakery.Interactor.Kind. func (*interactor) Kind() string { return interactionMethod } // Interact implements httpbakery.Interactor.Interact. func (i *interactor) Interact(ctx context.Context, client *httpbakery.Client, location string, ierr *httpbakery.Error) (*httpbakery.DischargeToken, error) { var info interactionInfo if err := ierr.InteractionMethod(interactionMethod, &info); err != nil { return nil, errgo.Mask(err, errgo.Is(httpbakery.ErrInteractionMethodNotFound)) } var resp LoginResponse if err := i.interact(ctx, &httprequest.Client{Doer: client}, info.URL, &resp); err != nil { return nil, errgo.Mask(err, errgo.Any) } return resp.DischargeToken, nil } // LegacyInteract implements httpbakery.LegacyInteractor.LegacyInteract. func (i *interactor) LegacyInteract(ctx context.Context, client *httpbakery.Client, location string, u *url.URL) error { return errgo.Mask(i.interact(ctx, &httprequest.Client{Doer: client}, u.String(), nil), errgo.Any) } func (i *interactor) interact(ctx context.Context, client *httprequest.Client, url string, resp interface{}) error { tok, err := i.tg.GetToken(ctx) if err != nil { return errgo.NoteMask(err, "cannot get token", errgo.Any) } req, err := http.NewRequest("GET", url, nil) if err != nil { return errgo.Notef(err, "cannot create request") } base := *req.URL base.RawQuery = "" rp := usso.RequestParameters{ HTTPMethod: req.Method, BaseURL: base.String(), Params: req.URL.Query(), SignatureMethod: usso.HMACSHA1{}, } if err := tok.SignRequest(&rp, req); err != nil { return errgo.Notef(err, "cannot sign request") } if err := client.Do(ctx, req, resp); err != nil { return errgo.Mask(err) } return nil } // A LoginResponse is a response from the login endpoint following a // successful interaction. type LoginResponse struct { DischargeToken *httpbakery.DischargeToken `json:"discharge-token"` } golang-github-canonical-candid-1.12.3/candidclient/ussologin/visitwebpage_test.go000066400000000000000000000076221457263123000302260ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package ussologin_test import ( "context" "net/http" "net/http/httptest" "testing" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/usso" errgo "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/canonical/candid/candidclient/ussologin" ) func TestKind(t *testing.T) { c := qt.New(t) defer c.Done() i := ussologin.NewInteractor(nil) c.Assert(i.Kind(), qt.Equals, "usso_oauth") } func TestInteractNotSupportedError(t *testing.T) { c := qt.New(t) defer c.Done() i := ussologin.NewInteractor(nil) req, err := http.NewRequest("GET", "", nil) c.Assert(err, qt.IsNil) ierr := httpbakery.NewInteractionRequiredError(nil, req) httpbakery.SetLegacyInteraction(ierr, "", "") _, err = i.Interact(context.Background(), nil, "", ierr) c.Assert(errgo.Cause(err), qt.Equals, httpbakery.ErrInteractionMethodNotFound) } func TestInteractGetTokenError(t *testing.T) { c := qt.New(t) defer c.Done() terr := errgo.New("test error") i := ussologin.NewInteractor(tokenGetterFunc(func(_ context.Context) (*usso.SSOData, error) { return nil, terr })) ierr := interactionRequiredError(c, "") _, err := i.Interact(context.Background(), nil, "", ierr) c.Assert(errgo.Cause(err), qt.Equals, terr) } func TestAuthenticatedRequest(t *testing.T) { c := qt.New(t) defer c.Done() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { // Just check the request has a correct looking // Authorization header, we won't check the signature. c.Check(req.Header.Get("Authorization"), qt.Matches, "OAuth .*") httprequest.WriteJSON(w, http.StatusOK, ussologin.LoginResponse{ DischargeToken: &httpbakery.DischargeToken{ Kind: "test", Value: []byte("test-token"), }, }) })) defer server.Close() i := ussologin.NewInteractor(tokenGetterFunc(func(_ context.Context) (*usso.SSOData, error) { return &usso.SSOData{ ConsumerKey: "test-user", ConsumerSecret: "test-user-secret", Realm: "test", TokenKey: "test-token", TokenName: "test", TokenSecret: "test-token-secret", }, nil })) ierr := interactionRequiredError(c, server.URL) dt, err := i.Interact(context.Background(), httpbakery.NewClient(), "", ierr) c.Assert(err, qt.IsNil) c.Assert(dt, qt.DeepEquals, &httpbakery.DischargeToken{ Kind: "test", Value: []byte("test-token"), }) } func TestAuthenticatedRequestError(t *testing.T) { c := qt.New(t) defer c.Done() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { // Just check the request has a correct looking // Authorization header, we won't check the signature. c.Check(req.Header.Get("Authorization"), qt.Matches, "OAuth .*") code, body := httpbakery.ErrorToResponse(context.Background(), errgo.New("test error")) httprequest.WriteJSON(w, code, body) })) defer server.Close() i := ussologin.NewInteractor(tokenGetterFunc(func(_ context.Context) (*usso.SSOData, error) { return &usso.SSOData{ ConsumerKey: "test-user", ConsumerSecret: "test-user-secret", Realm: "test", TokenKey: "test-token", TokenName: "test", TokenSecret: "test-token-secret", }, nil })) ierr := interactionRequiredError(c, server.URL) _, err := i.Interact(context.Background(), httpbakery.NewClient(), "", ierr) c.Assert(err, qt.ErrorMatches, `Get http.*: test error`) } func interactionRequiredError(c *qt.C, url string) *httpbakery.Error { req, err := http.NewRequest("GET", "", nil) c.Assert(err, qt.IsNil) ierr := httpbakery.NewInteractionRequiredError(nil, req) ussologin.SetInteraction(ierr, url) return ierr } type tokenGetterFunc func(ctx context.Context) (*usso.SSOData, error) func (f tokenGetterFunc) GetToken(ctx context.Context) (*usso.SSOData, error) { return f(ctx) } golang-github-canonical-candid-1.12.3/candidtest/000077500000000000000000000000001457263123000216175ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/candidtest/candidtest.go000066400000000000000000000076141457263123000243000ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package candidtest provides an inmemory candid service for use in // tests. package candidtest import ( "context" "net/http/httptest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/aclstore/v2" "github.com/juju/simplekv/memsimplekv" "github.com/canonical/candid" "github.com/canonical/candid/store" "github.com/canonical/candid/store/memstore" ) type Testing interface { // Cleanup is used to cleanup reseourcs created as part of the test. Cleanup(func()) // Fatalf is used to stop the test when a fatal error occurs. Fatalf(f string, args ...interface{}) } // Serve starts a new candid server using the given parameters. Any // required parameters that are not spedified will use appropriate // defaults. The given API versions will be added to the server, if no // API versions are specified then all available versions will be used. // Serve uses the Cleanup method on the given Testing object to defer // cleaning up any reseources that it creates. func Serve(t Testing, p candid.ServerParams, versions ...string) *httptest.Server { srv := httptest.NewUnstartedServer(nil) if p.Location == "" { p.Location = "http://" + srv.Listener.Addr().String() } srv.Config.Handler = initServer(t, p, versions...) srv.Start() return srv } // ServeTLS starts a new candid server using the given parameters on a // TLS server. Any required parameters that are not spedified will use // appropriate defaults. The given API versions will be added to the // server, if no API versions are specified then all available versions // will be used. Serve uses the Cleanup method on the given Testing // object to defer cleaning up any reseources that it creates. func ServeTLS(t Testing, p candid.ServerParams, versions ...string) *httptest.Server { srv := httptest.NewUnstartedServer(nil) if p.Location == "" { p.Location = "https://" + srv.Listener.Addr().String() } srv.Config.Handler = initServer(t, p, versions...) srv.StartTLS() return srv } func initServer(t Testing, p candid.ServerParams, versions ...string) candid.HandlerCloser { if p.MeetingStore == nil { p.MeetingStore = memstore.NewMeetingStore() } if p.ProviderDataStore == nil { p.ProviderDataStore = memstore.NewProviderDataStore() } if p.RootKeyStore == nil { p.RootKeyStore = bakery.NewMemRootKeyStore() } if p.Store == nil { p.Store = memstore.NewStore() } if p.Key == nil { var err error p.Key, err = bakery.GenerateKey() if err != nil { t.Fatalf("cannot generate key: %s", err) } } if p.ACLStore == nil { p.ACLStore = aclstore.NewACLStore(memsimplekv.NewStore()) } if p.PrivateAddr == "" { p.PrivateAddr = "127.0.0.1" } if len(versions) == 0 { versions = candid.Versions() } hnd, err := candid.NewServer(p, versions...) if err != nil { t.Fatalf("cannot create server: %s", err) } t.Cleanup(hnd.Close) return hnd } // AddIdentity adds a new identity to the given store. If there is an // error adding the identity AddIdentity will panic. func AddIdentity(ctx context.Context, st store.Store, identity *store.Identity) { update := store.Update{ store.Username: store.Set, } if identity.Name != "" { update[store.Name] = store.Set } if identity.Email != "" { update[store.Email] = store.Set } if len(identity.Groups) > 0 { update[store.Groups] = store.Set } if len(identity.PublicKeys) > 0 { update[store.PublicKeys] = store.Set } if !identity.LastLogin.IsZero() { update[store.LastLogin] = store.Set } if !identity.LastDischarge.IsZero() { update[store.LastDischarge] = store.Set } if len(identity.ProviderInfo) > 0 { update[store.ProviderInfo] = store.Set } if len(identity.ExtraInfo) > 0 { update[store.ExtraInfo] = store.Set } if identity.Owner != "" { update[store.Owner] = store.Set } if err := st.UpdateIdentity(ctx, identity, update); err != nil { panic(err) } } golang-github-canonical-candid-1.12.3/candidtest/checkers.go000066400000000000000000000022561457263123000237420ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package candidtest import ( "context" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" errgo "gopkg.in/errgo.v1" ) const candidtestNamespace = "github.com/juju/candidclient/candidtest" var checker = newChecker() func newChecker() *checkers.Checker { ch := checkers.New(nil) ch.Namespace().Register(candidtestNamespace, "candidtest") ch.Register("discharge-id", candidtestNamespace, checkDischargeID) return ch } type dischargeIDKey struct{} func contextWithDischargeID(ctx context.Context, dischargeID string) context.Context { return context.WithValue(ctx, dischargeIDKey{}, dischargeID) } func dischargeIDFromContext(ctx context.Context) string { dischargeID, _ := ctx.Value(dischargeIDKey{}).(string) return dischargeID } func dischargeIDCaveat(dischargeID string) checkers.Caveat { return checkers.Caveat{ Condition: "discharge-id " + dischargeID, Namespace: candidtestNamespace, } } func checkDischargeID(ctx context.Context, cond, arg string) error { if dischargeIDFromContext(ctx) == arg { return nil } return errgo.New("incorrect discharge id") } golang-github-canonical-candid-1.12.3/candidtest/identity.go000066400000000000000000000031601457263123000237770ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package candidtest import ( "context" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/candidclient" ) // identityClient implement identchecker.IdentityClient. This is used because // the candidtest server cannot use candidclient.Client because that uses the // groups endpoint, which cannot be used because that would lead to an // infinite recursion. type identityClient struct { srv *Server } func (i identityClient) IdentityFromContext(ctx context.Context) (identchecker.Identity, []checkers.Caveat, error) { return nil, candidclient.IdentityCaveats(i.srv.URL.String()), nil } func (i identityClient) DeclaredIdentity(ctx context.Context, declared map[string]string) (identchecker.Identity, error) { username := declared["username"] if username == "" { return nil, errgo.Newf("no declared user name in %q", declared) } return &identity{ srv: i.srv, id: username, }, nil } type identity struct { srv *Server id string } func (i identity) Id() string { return i.id } func (i identity) Domain() string { return "" } // Allow implements identchecker.ACLIdentity.Allow. func (i identity) Allow(_ context.Context, acl []string) (bool, error) { groups := []string{i.id} u := i.srv.users[i.id] if u != nil { groups = append(groups, u.groups...) } for _, g1 := range groups { for _, g2 := range acl { if g1 == g2 { return true, nil } } } return false, nil } golang-github-canonical-candid-1.12.3/candidtest/server.go000066400000000000000000000266401457263123000234640ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. // Package candidtest holds a mock implementation of the identity manager // suitable for testing. package candidtest import ( "context" "crypto/sha256" "fmt" "net/http" "net/url" "strings" "sync" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakerytest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" macaroon "gopkg.in/macaroon.v2" "github.com/canonical/candid/candidclient" "github.com/canonical/candid/params" ) // GroupListGroup is the group that users must belong to in order to // enquire about other users' groups. const GroupListGroup = "group-lister" // Server represents a mock identity server. // It currently serves only the discharge and groups endpoints. type Server struct { // URL holds the URL of the mock identity server. // The discharger endpoint is located at URL/v1/discharge. URL *url.URL // PublicKey holds the public key of the mock identity server. PublicKey *bakery.PublicKey // Bakery holds the macaroon bakery used by // the mock server. Bakery *identchecker.Bakery discharger *bakerytest.Discharger // mu guards the fields below it. mu sync.Mutex users map[string]*user defaultUser string } type user struct { groups []string key *bakery.KeyPair } // NewServer runs a mock identity server. It can discharge // macaroons and return information on user group membership. // The returned server should be closed after use. func NewServer() *Server { srv := &Server{ users: make(map[string]*user), } srv.discharger = bakerytest.NewDischarger(nil) srv.discharger.Checker = httpbakery.ThirdPartyCaveatCheckerFunc(srv.checkThirdPartyCaveat) u, err := url.Parse(srv.discharger.Location()) if err != nil { panic(err) } srv.URL = u key, err := bakery.GenerateKey() if err != nil { panic(err) } srv.PublicKey = &key.Public srv.discharger.AddHTTPHandlers(reqServer.Handlers(srv.newHandler)) srv.Bakery = identchecker.NewBakery(identchecker.BakeryParams{ Checker: checker, Locator: srv, Key: key, IdentityClient: identityClient{srv}, Authorizer: identchecker.ACLAuthorizer{ GetACL: srv.getACL, }, }) return srv } var reqServer = httprequest.Server{ ErrorMapper: errToResp, } func errToResp(ctx context.Context, err error) (int, interface{}) { // Allow bakery errors to be returned as the bakery would // like them, so that httpbakery.Client.Do will work. if err, ok := errgo.Cause(err).(*httpbakery.Error); ok { return httpbakery.ErrorToResponse(ctx, err) } errorBody := errorResponseBody(err) status := http.StatusInternalServerError switch errorBody.Code { case params.ErrNotFound: status = http.StatusNotFound case params.ErrForbidden, params.ErrAlreadyExists: status = http.StatusForbidden case params.ErrBadRequest: status = http.StatusBadRequest case params.ErrUnauthorized, params.ErrNoAdminCredsProvided: status = http.StatusUnauthorized case params.ErrMethodNotAllowed: status = http.StatusMethodNotAllowed case params.ErrServiceUnavailable: status = http.StatusServiceUnavailable } return status, errorBody } // errorResponse returns an appropriate error response for the provided error. func errorResponseBody(err error) *params.Error { errResp := ¶ms.Error{ Message: err.Error(), } cause := errgo.Cause(err) if coder, ok := cause.(errorCoder); ok { errResp.Code = coder.ErrorCode() } else if errgo.Cause(err) == httprequest.ErrUnmarshal { errResp.Code = params.ErrBadRequest } return errResp } type errorCoder interface { ErrorCode() params.ErrorCode } // Close shuts down the server. func (srv *Server) Close() { srv.discharger.Close() } // PublicKeyForLocation implements bakery.PublicKeyLocator // by returning the server's public key for all locations. func (srv *Server) PublicKeyForLocation(loc string) (*bakery.PublicKey, error) { return srv.PublicKey, nil } // ThirdPartyInfo implements bakery.ThirdPartyLocator.ThirdPartyInfo. func (srv *Server) ThirdPartyInfo(ctx context.Context, loc string) (bakery.ThirdPartyInfo, error) { return srv.discharger.ThirdPartyInfo(ctx, loc) } // UserPublicKey returns the key for the given user. // It panics if the user has not been added. func (srv *Server) UserPublicKey(username string) *bakery.KeyPair { u := srv.user(username) if u == nil { panic("no user found") } return u.key } // CandidClient returns an identity manager client that takes // to the given server as the given user name. func (srv *Server) CandidClient(username string) *candidclient.Client { c, err := candidclient.New(candidclient.NewParams{ BaseURL: srv.URL.String(), AgentUsername: username, Client: srv.Client(username), }) if err != nil { panic(err) } return c } // Client returns a bakery client that will discharge as the given user. // If the user does not exist, it is added with no groups. func (srv *Server) Client(username string) *httpbakery.Client { c := httpbakery.NewClient() u := srv.user(username) if u == nil { srv.AddUser(username) u = srv.user(username) } c.Key = u.key // Note that this duplicates the SetUpAuth that candidclient.New will do // but that shouldn't matter as SetUpAuth is idempotent. agent.SetUpAuth(c, &agent.AuthInfo{ Key: u.key, Agents: []agent.Agent{{ URL: srv.URL.String(), Username: username, }}, }) return c } // SetDefaultUser configures the server so that it will discharge for // the given user if no agent-login cookie is found. The user does not // need to have been added. Note that this will bypass the // VisitURL logic. // // If the name is empty, there will be no default user. func (srv *Server) SetDefaultUser(name string) { srv.mu.Lock() defer srv.mu.Unlock() srv.defaultUser = name } // AddUser adds a new user that's in the given set of groups. // If the user already exists, the given groups are // added to that user's groups. func (srv *Server) AddUser(name string, groups ...string) { srv.mu.Lock() defer srv.mu.Unlock() u := srv.users[name] if u == nil { key, err := bakery.GenerateKey() if err != nil { panic(err) } srv.users[name] = &user{ groups: groups, key: key, } return } for _, g := range groups { found := false for _, ug := range u.groups { if ug == g { found = true break } } if !found { u.groups = append(u.groups, g) } } } // RemoveUsers removes all added users and resets the // default user to nothing. func (srv *Server) RemoveUsers() { srv.mu.Lock() defer srv.mu.Unlock() srv.users = make(map[string]*user) srv.defaultUser = "" } // RemoveUser removes the given user. func (srv *Server) RemoveUser(user string) { srv.mu.Lock() defer srv.mu.Unlock() delete(srv.users, user) } func (srv *Server) user(name string) *user { srv.mu.Lock() defer srv.mu.Unlock() return srv.users[name] } func (srv *Server) getACL(ctx context.Context, op bakery.Op) ([]string, bool, error) { switch op.Action { case "login": return []string{identchecker.Everyone}, true, nil case "list-groups": return []string{strings.TrimPrefix(op.Entity, "user-"), GroupListGroup}, true, nil default: return nil, false, errgo.New("unrecognised operation") } } func (srv *Server) checkThirdPartyCaveat(ctx context.Context, req *http.Request, info *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { if srv.defaultUser != "" { return []checkers.Caveat{ candidclient.UserDeclaration(srv.defaultUser), }, nil } dischargeID := srv.dischargeID(info) ctx = contextWithDischargeID(ctx, dischargeID) if token == nil || token.Kind != "agent" { ierr := httpbakery.NewInteractionRequiredError(nil, req) agent.SetInteraction(ierr, "/login/agent?discharge-id="+dischargeID) return nil, ierr } var ms macaroon.Slice if err := ms.UnmarshalBinary(token.Value); err != nil { return nil, errgo.Mask(err) } ops, _, err := srv.Bakery.Oven.VerifyMacaroon(ctx, ms) if err != nil { return nil, errgo.Mask(err) } username := "" for _, op := range ops { if strings.HasPrefix(op.Entity, "user-") && op.Action == "discharge" { username = strings.TrimPrefix(op.Entity, "user-") break } } _, err = srv.Bakery.Checker.Auth(ms).Allow( ctx, bakery.Op{ Entity: "user-" + username, Action: "discharge", }, ) if err != nil { return nil, errgo.Mask(err) } return []checkers.Caveat{ candidclient.UserDeclaration(username), }, nil } func (srv *Server) dischargeID(info *bakery.ThirdPartyCaveatInfo) string { sum := sha256.Sum256(info.Caveat) return fmt.Sprintf("%x", sum[:4]) } func (srv *Server) newHandler(p httprequest.Params, req interface{}) (*handler, context.Context, error) { _, err := srv.Bakery.Checker.Auth(httpbakery.RequestMacaroons(p.Request)...).Allow(p.Context, srv.opForRequest(req)) if err == nil { return &handler{srv}, p.Context, nil } derr, ok := errgo.Cause(err).(*bakery.DischargeRequiredError) if !ok { return nil, p.Context, errgo.Mask(err) } version := httpbakery.RequestVersion(p.Request) m, err := srv.Bakery.Oven.NewMacaroon(p.Context, version, derr.Caveats, derr.Ops...) if err != nil { return nil, p.Context, errgo.Notef(err, "cannot create macaroon") } return nil, p.Context, httpbakery.NewDischargeRequiredError(httpbakery.DischargeRequiredErrorParams{ Macaroon: m, OriginalError: err, Request: p.Request, }) } func (srv *Server) opForRequest(req interface{}) bakery.Op { switch r := req.(type) { case *agentMacaroonRequest: return agentLoginOp case *groupsRequest: return bakery.Op{ Entity: "user-" + r.User, Action: "list-groups", } default: panic("unrecognised request") } } var agentLoginOp = bakery.Op{ Entity: "agent", Action: "login", } type handler struct { srv *Server } type groupsRequest struct { httprequest.Route `httprequest:"GET /v1/u/:User/groups"` User string `httprequest:",path"` } func (h handler) GetGroups(p httprequest.Params, req *groupsRequest) ([]string, error) { if u := h.srv.user(req.User); u != nil { return u.groups, nil } return nil, params.ErrNotFound } // agentMacaroonRequest represents a request to get the // agent macaroon that, when discharged, becomes // the discharge token to complete the discharge. type agentMacaroonRequest struct { httprequest.Route `httprequest:"GET /login/agent"` Username string `httprequest:"username,form"` PublicKey *bakery.PublicKey `httprequest:"public-key,form"` DischargeID string `httprequest:"discharge-id,form"` } type agentMacaroonResponse struct { Macaroon *bakery.Macaroon `json:"macaroon"` } // Visit implements http.Handler. It performs the agent login interaction flow. func (h handler) Visit(p httprequest.Params, req *agentMacaroonRequest) (*agentMacaroonResponse, error) { m, err := h.srv.Bakery.Oven.NewMacaroon( p.Context, httpbakery.RequestVersion(p.Request), []checkers.Caveat{ dischargeIDCaveat(req.DischargeID), bakery.LocalThirdPartyCaveat(req.PublicKey, httpbakery.RequestVersion(p.Request)), }, bakery.Op{ Entity: "user-" + req.Username, Action: "discharge", }, ) if err != nil { return nil, errgo.Mask(err) } return &agentMacaroonResponse{ Macaroon: m, }, nil } golang-github-canonical-candid-1.12.3/candidtest/server_test.go000066400000000000000000000076361457263123000245270ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package candidtest_test import ( "context" "testing" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/canonical/candid/candidclient" "github.com/canonical/candid/candidtest" candidparams "github.com/canonical/candid/params" ) func TestDischarge(t *testing.T) { c := qt.New(t) defer c.Done() ctx := context.TODO() srv := candidtest.NewServer() srv.AddUser("server-user", candidtest.GroupListGroup) srv.AddUser("bob", "somegroup") client := srv.Client("bob") key, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) b := identchecker.NewBakery(identchecker.BakeryParams{ Key: key, Locator: srv, IdentityClient: srv.CandidClient("server-user"), }) m, err := b.Oven.NewMacaroon( ctx, bakery.LatestVersion, candidclient.IdentityCaveats(srv.URL.String()), identchecker.LoginOp, ) c.Assert(err, qt.IsNil) ms, err := client.DischargeAll(ctx, m) c.Assert(err, qt.IsNil) // Make sure that the macaroon discharged correctly and that it // has the right declared caveats. authInfo, err := b.Checker.Auth(ms).Allow(ctx, identchecker.LoginOp) c.Assert(err, qt.IsNil) c.Assert(authInfo.Identity, qt.Not(qt.IsNil)) ident := authInfo.Identity.(candidclient.Identity) c.Assert(ident.Id(), qt.Equals, "bob") username, err := ident.Username() c.Assert(err, qt.IsNil) c.Assert(username, qt.Equals, "bob") groups, err := ident.Groups() c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"somegroup"}) } func TestDischargeDefaultUser(t *testing.T) { c := qt.New(t) defer c.Done() ctx := context.TODO() srv := candidtest.NewServer() srv.SetDefaultUser("bob") key, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) b := identchecker.NewBakery(identchecker.BakeryParams{ Key: key, Locator: srv, IdentityClient: srv.CandidClient("server-user"), }) m, err := b.Oven.NewMacaroon( ctx, bakery.LatestVersion, candidclient.IdentityCaveats(srv.URL.String()), identchecker.LoginOp, ) c.Assert(err, qt.IsNil) client := httpbakery.NewClient() ms, err := client.DischargeAll(ctx, m) c.Assert(err, qt.IsNil) // Make sure that the macaroon discharged correctly and that it // has the right declared caveats. authInfo, err := b.Checker.Auth(ms).Allow(ctx, identchecker.LoginOp) c.Assert(err, qt.IsNil) c.Assert(authInfo.Identity, qt.Not(qt.IsNil)) ident := authInfo.Identity.(candidclient.Identity) c.Assert(ident.Id(), qt.Equals, "bob") username, err := ident.Username() c.Assert(err, qt.IsNil) c.Assert(username, qt.Equals, "bob") groups, err := ident.Groups() c.Assert(err, qt.IsNil) c.Assert(groups, qt.HasLen, 0) } func TestGroups(t *testing.T) { c := qt.New(t) defer c.Done() srv := candidtest.NewServer() srv.AddUser("server-user", candidtest.GroupListGroup) srv.AddUser("bob", "beatles", "bobbins") srv.AddUser("alice") client := srv.CandidClient("server-user") groups, err := client.UserGroups(context.TODO(), &candidparams.UserGroupsRequest{ Username: "bob", }) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"beatles", "bobbins"}) groups, err = client.UserGroups(context.TODO(), &candidparams.UserGroupsRequest{ Username: "alice", }) c.Assert(err, qt.IsNil) c.Assert(groups, qt.HasLen, 0) } func TestAddUserWithExistingGroups(t *testing.T) { c := qt.New(t) defer c.Done() srv := candidtest.NewServer() srv.AddUser("alice", "anteaters") srv.AddUser("alice") srv.AddUser("alice", "goof", "anteaters") client := srv.CandidClient("alice") groups, err := client.UserGroups(context.TODO(), &candidparams.UserGroupsRequest{ Username: "alice", }) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"anteaters", "goof"}) } golang-github-canonical-candid-1.12.3/charms/000077500000000000000000000000001457263123000207525ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid-k8s/000077500000000000000000000000001457263123000226775ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid-k8s/.flake8000066400000000000000000000001521457263123000240500ustar00rootroot00000000000000[flake8] max-line-length = 99 select: E,W,F,C,N exclude: .tox venv .git build dist *.egg_info golang-github-canonical-candid-1.12.3/charms/candid-k8s/.gitignore000066400000000000000000000000751457263123000246710ustar00rootroot00000000000000venv/ .tox/ build/ *.charm .coverage __pycache__/ *.py[cod] golang-github-canonical-candid-1.12.3/charms/candid-k8s/.jujuignore000066400000000000000000000000301457263123000250520ustar00rootroot00000000000000/venv *.py[cod] *.charm golang-github-canonical-candid-1.12.3/charms/candid-k8s/CONTRIBUTING.md000066400000000000000000000010231457263123000251240ustar00rootroot00000000000000# candid-operator ## Developing Create and activate a virtualenv with the development requirements: virtualenv -p python3 venv source venv/bin/activate pip install -r requirements-dev.txt ## Intended use case The Candid operator charm is intended for deploying the Candid identity service into a k8s cluster. ## Roadmap * Add postgresql relation. ## Testing The Python operator framework includes a very nice harness for testing operator behaviour without full deployment. Just `run_tests` : ./run_tests golang-github-canonical-candid-1.12.3/charms/candid-k8s/LICENSE000066400000000000000000000261361457263123000237140ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. golang-github-canonical-candid-1.12.3/charms/candid-k8s/README.md000066400000000000000000000003151457263123000241550ustar00rootroot00000000000000# Candid Identity service The Candid server provides a macaroon-based authentication service. The charm is designed to by in a model using haproxy as a frontend and storing data in a postgresql database. golang-github-canonical-candid-1.12.3/charms/candid-k8s/actions.yaml000066400000000000000000000001111457263123000252140ustar00rootroot00000000000000# Copyright 2022 Canonical Ltd # See LICENSE file for licensing details. golang-github-canonical-candid-1.12.3/charms/candid-k8s/charmcraft.yaml000066400000000000000000000003511457263123000256740ustar00rootroot00000000000000# Learn more about charmcraft.yaml configuration at: # https://juju.is/docs/sdk/charmcraft-config type: "charm" bases: - build-on: - name: "ubuntu" channel: "20.04" run-on: - name: "ubuntu" channel: "20.04" golang-github-canonical-candid-1.12.3/charms/candid-k8s/config.yaml000066400000000000000000000067761457263123000250500ustar00rootroot00000000000000# Copyright 2022 Canonical Ltd # See LICENSE file for licensing details. options: admin-agent-public-key: type: string default: "" description: | Base64 encoded 256-bit Ed25519 public key for admin agent. api-macaroon-timeout: type: string default: "48h" description: | The maximum age an API macaroon can get before requiring re-authorization. discharge-macaroon-timeout: type: string default: "48h" description: | The maximum age a discharge macaroon can get before it becomes invalid. discharge-token-timeout: type: string default: "48h" description: | The maximum age a discharge token can get before it becomes invalid. enable-email-login: type: boolean default: false description: | Enable the "login with email address" functionality on the authentication required page. http-proxy: type: string default: "" description: Address of proxy to use for outgoing HTTP connections. identity-providers: type: string default: "" description: | This is a Base64 encoded YAML array containing the identity provider definition. See https://github.com/CanonicalLtd/candid/blob/master/docs/configuration.md#identity-providers-1 for a full list of possible options. location: type: string default: "" description: | Publicly accessable URL of the identity manager (defaults to public address of unit). logging-config: type: string default: INFO description: Loggo logging configuration string. no-proxy: type: string default: "" description: | List of addresses that should not use the proxy specified in http-proxy. If specified this should be a comma-separated list of addresses. private-key: type: string default: "" description: Base64 encoded 256-bit Ed25519 private key of the server. public-key: type: string default: "" description: | Base64 encoded 256-bit Ed25519 public key, this should match the private key. redirect-login-trusted-urls: type: string default: "" description: | List of URLs that are trusted to return to when using the redirect login flow. redirect-login-trusted-domains: type: string default: "" description: | Comma separated list of domains for which all redirect paths are trusted. If a domain starts with "*." that is taken to be a wildcard and will match all subdomains of the specified domain. rendezvous-timeout: type: string default: "10m" description: | Amount of time that an interactive authentication request can be active before it is forgotten. The value must be a time duration specified as a decimal number followed by a unit from ns, us, ms, s, m, h for the time units between nanosecond and hour. skip-location-for-cookie-paths: type: boolean default: false description: | If true, it leaves cookies' Path value absolute, instead of seeting it relative to the path in the location config. mfa-rp-display-name: type: string default: "" description: | Display name for the multi-factor authentication relying party. mfa-rp-id: type: string default: "" description: | ID of the multi-factor authentication relying party (usually the FQDN for your site). mfa-rp-origin: type: string default: "" description: | Origin of the multi-factor authentication WebAuthn requests. golang-github-canonical-candid-1.12.3/charms/candid-k8s/lib/000077500000000000000000000000001457263123000234455ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid-k8s/lib/charms/000077500000000000000000000000001457263123000247225ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid-k8s/lib/charms/nginx_ingress_integrator/000077500000000000000000000000001457263123000320355ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid-k8s/lib/charms/nginx_ingress_integrator/v0/000077500000000000000000000000001457263123000323625ustar00rootroot00000000000000ingress.py000066400000000000000000000163741457263123000343420ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid-k8s/lib/charms/nginx_ingress_integrator/v0"""Library for the ingress relation. This library contains the Requires and Provides classes for handling the ingress interface. Import `IngressRequires` in your charm, with two required options: - "self" (the charm itself) - config_dict `config_dict` accepts the following keys: - service-hostname (required) - service-name (required) - service-port (required) - additional-hostnames - limit-rps - limit-whitelist - max-body-size - owasp-modsecurity-crs - path-routes - retry-errors - rewrite-enabled - rewrite-target - service-namespace - session-cookie-max-age - tls-secret-name See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions of each, along with the required type. As an example, add the following to `src/charm.py`: ``` from charms.nginx_ingress_integrator.v0.ingress import IngressRequires # In your charm's `__init__` method. self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"], "service-name": self.app.name, "service-port": 80}) # In your charm's `config-changed` handler. self.ingress.update_config({"service-hostname": self.config["external_hostname"]}) ``` And then add the following to `metadata.yaml`: ``` requires: ingress: interface: ingress ``` You _must_ register the IngressRequires class as part of the `__init__` method rather than, for instance, a config-changed event handler. This is because doing so won't get the current relation changed event, because it wasn't registered to handle the event (because it wasn't created in `__init__` when the event was fired). """ import logging from ops.charm import CharmEvents from ops.framework import EventBase, EventSource, Object from ops.model import BlockedStatus # The unique Charmhub library identifier, never change it LIBID = "db0af4367506491c91663468fb5caa4c" # Increment this major API version when introducing breaking changes LIBAPI = 0 # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version LIBPATCH = 10 logger = logging.getLogger(__name__) REQUIRED_INGRESS_RELATION_FIELDS = { "service-hostname", "service-name", "service-port", } OPTIONAL_INGRESS_RELATION_FIELDS = { "additional-hostnames", "limit-rps", "limit-whitelist", "max-body-size", "owasp-modsecurity-crs", "path-routes", "retry-errors", "rewrite-target", "rewrite-enabled", "service-namespace", "session-cookie-max-age", "tls-secret-name", } class IngressAvailableEvent(EventBase): pass class IngressBrokenEvent(EventBase): pass class IngressCharmEvents(CharmEvents): """Custom charm events.""" ingress_available = EventSource(IngressAvailableEvent) ingress_broken = EventSource(IngressBrokenEvent) class IngressRequires(Object): """This class defines the functionality for the 'requires' side of the 'ingress' relation. Hook events observed: - relation-changed """ def __init__(self, charm, config_dict): super().__init__(charm, "ingress") self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed) self.config_dict = config_dict def _config_dict_errors(self, update_only=False): """Check our config dict for errors.""" blocked_message = "Error in ingress relation, check `juju debug-log`" unknown = [ x for x in self.config_dict if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS ] if unknown: logger.error( "Ingress relation error, unknown key(s) in config dictionary found: %s", ", ".join(unknown), ) self.model.unit.status = BlockedStatus(blocked_message) return True if not update_only: missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict] if missing: logger.error( "Ingress relation error, missing required key(s) in config dictionary: %s", ", ".join(sorted(missing)), ) self.model.unit.status = BlockedStatus(blocked_message) return True return False def _on_relation_changed(self, event): """Handle the relation-changed event.""" # `self.unit` isn't available here, so use `self.model.unit`. if self.model.unit.is_leader(): if self._config_dict_errors(): return for key in self.config_dict: event.relation.data[self.model.app][key] = str(self.config_dict[key]) def update_config(self, config_dict): """Allow for updates to relation.""" if self.model.unit.is_leader(): self.config_dict = config_dict if self._config_dict_errors(update_only=True): return relation = self.model.get_relation("ingress") if relation: for key in self.config_dict: relation.data[self.model.app][key] = str(self.config_dict[key]) class IngressProvides(Object): """This class defines the functionality for the 'provides' side of the 'ingress' relation. Hook events observed: - relation-changed """ def __init__(self, charm): super().__init__(charm, "ingress") # Observe the relation-changed hook event and bind # self.on_relation_changed() to handle the event. self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed) self.framework.observe(charm.on["ingress"].relation_broken, self._on_relation_broken) self.charm = charm def _on_relation_changed(self, event): """Handle a change to the ingress relation. Confirm we have the fields we expect to receive.""" # `self.unit` isn't available here, so use `self.model.unit`. if not self.model.unit.is_leader(): return ingress_data = { field: event.relation.data[event.app].get(field) for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS } missing_fields = sorted( [ field for field in REQUIRED_INGRESS_RELATION_FIELDS if ingress_data.get(field) is None ] ) if missing_fields: logger.error( "Missing required data fields for ingress relation: {}".format( ", ".join(missing_fields) ) ) self.model.unit.status = BlockedStatus( "Missing fields for ingress: {}".format(", ".join(missing_fields)) ) # Create an event that our charm can use to decide it's okay to # configure the ingress. self.charm.on.ingress_available.emit() def _on_relation_broken(self, _): """Handle a relation-broken event in the ingress relation.""" if not self.model.unit.is_leader(): return # Create an event that our charm can use to remove the ingress resource. self.charm.on.ingress_broken.emit() golang-github-canonical-candid-1.12.3/charms/candid-k8s/metadata.yaml000066400000000000000000000012701457263123000253430ustar00rootroot00000000000000# Copyright 2022 Canonical Ltd # See LICENSE file for licensing details. # For a complete list of supported options, see: # https://juju.is/docs/sdk/metadata-reference name: candid-k8s display-name: Candid summary: Candid identity server. maintainer: JAAS Developers description: | Candid macaroon-based authentication service. tags: - web_server - authenticator - authentication - identity peers: candid: interface: candid requires: ingress: interface: ingress db: interface: pgsql limit: 1 containers: candid: resource: candid-image resources: candid-image: type: oci-image description: OCI image for Candid. golang-github-canonical-candid-1.12.3/charms/candid-k8s/requirements-dev.txt000066400000000000000000000000441457263123000267350ustar00rootroot00000000000000-r requirements.txt coverage flake8 golang-github-canonical-candid-1.12.3/charms/candid-k8s/requirements.txt000066400000000000000000000000771457263123000261670ustar00rootroot00000000000000Jinja2 == 2.11.3 ops >= 1.4.0 ops-lib-pgsql markupsafe == 2.0.1golang-github-canonical-candid-1.12.3/charms/candid-k8s/run_tests000077500000000000000000000005421457263123000246540ustar00rootroot00000000000000#!/bin/sh -e # Copyright 2022 Canonical Ltd # See LICENSE file for licensing details. if [ -z "$VIRTUAL_ENV" -a -d venv/ ]; then . venv/bin/activate fi if [ -z "$PYTHONPATH" ]; then export PYTHONPATH="lib:src" else export PYTHONPATH="lib:src:$PYTHONPATH" fi flake8 coverage run --branch --source=src -m unittest -v "$@" coverage report -m golang-github-canonical-candid-1.12.3/charms/candid-k8s/src/000077500000000000000000000000001457263123000234665ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid-k8s/src/charm.py000077500000000000000000000350761457263123000251500ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright 2022 Canonical Ltd # See LICENSE file for licensing details. # # Learn more at: https://juju.is/docs/sdk """Charm the service. Refer to the following post for a quick-start guide that will help you develop a new k8s charm using the Operator Framework: https://discourse.charmhub.io/t/4208 """ import base64 import binascii import functools import json import logging import os import pgsql import yaml from charms.nginx_ingress_integrator.v0.ingress import IngressRequires from jinja2 import Environment, FileSystemLoader from ops import pebble from ops.charm import CharmBase from ops.framework import StoredState from ops.main import main from ops.model import ActiveStatus, BlockedStatus, WaitingStatus class IdentityProvidersParseError(Exception): """Error parsing identity provider configuration.""" pass logger = logging.getLogger(__name__) WORKLOAD_CONTAINER = "candid" REQUIRED_SETTINGS = [ "ADMIN_AGENT_PUBLIC_KEY", "API_MACAROON_TIMEOUT", "DISCHARGE_MACAROON_TIMEOUT", "DISCHARGE_TOKEN_TIMEOUT", "IDENTITY_PROVIDERS", "LOCATION", "PRIVATE_KEY", "PUBLIC_KEY", "RENDEZVOUS_TIMEOUT", "POSTGRESQL_DSN", ] def log_event_handler(method): @functools.wraps(method) def decorated(self, event): logger.debug("running {}".format(method.__name__)) try: return method(self, event) finally: logger.debug("completed {}".format(method.__name__)) return decorated class CandidOperatorCharm(CharmBase): """Charm the service.""" _stored = StoredState() def __init__(self, *args): super().__init__(*args) self.db = pgsql.PostgreSQLClient(self, "db") self.framework.observe( self.db.on.database_relation_joined, self._on_database_relation_joined, ) self.framework.observe( self.db.on.master_changed, self._on_master_changed ) self.framework.observe( self.on.candid_pebble_ready, self._on_candid_pebble_ready ) self.framework.observe(self.on.config_changed, self._on_config_changed) self.framework.observe(self.on.update_status, self._on_update_status) self.framework.observe(self.on.leader_elected, self._on_leader_elected) self.framework.observe( self.on.candid_relation_changed, self._on_candid_relation_changed ) self.framework.observe(self.on.start, self._on_start) self.framework.observe(self.on.stop, self._on_stop) hostname = self.config.get("location", "").lstrip("https://") self.ingress = IngressRequires( self, { "service-hostname": hostname, "service-name": self.app.name, "service-port": 8081, }, ) self._config_filename = "/root/config.yaml" self._stored.set_default(db_uri=None) @log_event_handler def _on_candid_pebble_ready(self, event): self._on_config_changed(event) @log_event_handler def _on_config_changed(self, event): self._update_workload({}, event) @log_event_handler def _on_update_status(self, _): """Update the status of the charm.""" self._ready() @log_event_handler def _on_start(self, _): """Start Candid.""" container = self.unit.get_container(WORKLOAD_CONTAINER) if container.can_connect(): plan = container.get_plan() if "candid" not in plan.services: self.unit.status = BlockedStatus( "waiting for configuration", ) return env_vars = plan.services.get("candid").environment for setting in REQUIRED_SETTINGS: if not env_vars.get(setting, ""): self.unit.status = BlockedStatus( "{} configuration value not set".format(setting), ) return container.start("candid") @log_event_handler def _on_stop(self, _): """Stop Candid.""" container = self.unit.get_container(WORKLOAD_CONTAINER) if self._ready() and container.can_connect(): container.stop() self.unit.status = WaitingStatus("stopped") @log_event_handler def _on_leader_elected(self, event): """Elected leader generates the keypair to be used by all units.""" if not self.unit.is_leader(): return candid_relation = self.model.get_relation("candid") if not candid_relation: return if "public-key" in candid_relation.data[self.app]: # if public and private keys are already set # there is nothing to do. return key = self._generate_keypair(event) candid_relation.data[self.app].update({"public-key": key["public"]}) candid_relation.data[self.app].update({"private-key": key["private"]}) self._update_workload({}, event) def _on_candid_relation_changed(self, event): data = event.relation.data[event.app] if data["public-key"] and data["private-key"]: # if public and private keys are already set # there is nothing to do. return self._update_workload({}, event) def _on_database_relation_joined( self, event: pgsql.DatabaseRelationJoinedEvent ) -> None: """ Handles determining if the database has finished setup, once setup is complete a master/standby may join / change in consequent events. """ logging.info("(postgresql) RELATION_JOINED event fired.") if self.model.unit.is_leader(): event.database = "candid" elif event.database != "candid": event.defer() def _on_master_changed(self, event: pgsql.MasterChangedEvent) -> None: """ Handles master units of postgres joining / changing. The internal snap configuration is updated to reflect this. """ logging.info("(postgresql) MASTER_CHANGED event fired.") if event.database != "candid": logging.debug("Database setup not complete yet, returning.") return if event.master: self._stored.db_uri = str(event.master.uri) self._update_workload({"POSTGRESQL_DSN": self._stored.db_uri}, event) def _update_workload(self, envdata: dict, event): """' Update workload with all available configuration data.""" hostname = self.config.get("location", "").lstrip("https://") self.ingress.update_config({"service-hostname": hostname}) container = self.unit.get_container(WORKLOAD_CONTAINER) private_key = self.config.get("private-key", "") public_key = self.config.get("public-key", "") candid_relation = self.model.get_relation("candid") if candid_relation: private_key = candid_relation.data[self.app].get("private-key", "") public_key = candid_relation.data[self.app].get("public-key", "") config_values = { "ADMIN_AGENT_PUBLIC_KEY": self.config.get( "admin-agent-public-key", "" ), "API_MACAROON_TIMEOUT": self.config.get( "api-macaroon-timeout", "" ), "DISCHARGE_MACAROON_TIMEOUT": self.config.get( "discharge-macaroon-timeout", "" ), "DISCHARGE_TOKEN_TIMEOUT": self.config.get( "discharge-token-timeout", "" ), "ENABLE_EMAIL_LOGIN": self.config.get("enable-email-login", False), "HTTP_PROXY": self.config.get("http-proxy"), "LOCATION": self.config.get("location"), "LOGGING_CONFIG": self.config.get("logging-config"), "IDENTITY_PROVIDERS": self.config.get("identity-providers"), "NO_PROXY": self.config.get("no-proxy", ""), "PRIVATE_KEY": private_key, "PUBLIC_KEY": public_key, "REDIRECT_LOGIN_TRUSTED_URLS": self.config.get( "redirect-login-trusted-urls", "" ), "REDIRECT_LOGIN_TRUSTED_DOMAINS": self.config.get( "redirect-login-trusted-domains", "" ), "RENDEZVOUS_TIMEOUT": self.config.get("rendezvous-timeout"), "SKIP_LOCATION_FOR_COOKIE_PATHS": self.config.get( "skip-location-for-cookie-paths", False ), "MFA_RP_DISPLAY_NAME": self.config.get("mfa-rp-display-name", ""), "MFA_RP_ID": self.config.get("mfa-rp-id", ""), "MFA_RP_ORIGIN": self.config.get("mfa-rp-origin", ""), "POSTGRESQL_DSN": self._stored.db_uri, } # apply specified environment data config_values.update(envdata) # remove empty configuration values config_values = { key: value for key, value in config_values.items() if value } # if private and public keys are not set, then # we check the candid relation data if the leader # already generated a key candid_relation = self.model.get_relation("candid") if "PUBLIC_KEY" not in config_values and candid_relation: config_values["PUBLIC_KEY"] = candid_relation.data[self.app].get( "public-key", "" ) if "PRIVATE_KEY" not in config_values and candid_relation: config_values["PRIVATE_KEY"] = candid_relation.data[self.app].get( "private-key", "" ) # extend no-proxy to include all candid units. no_proxy = [] if "NO_PROXY" in config_values: no_proxy = [config_values["NO_PROXY"]] if candid_relation: for unit in candid_relation.units: if unit not in candid_relation.data: continue if "private-address" in candid_relation.data[unit]: no_proxy.append( candid_relation.data[unit].get("private-address") ) if no_proxy: config_values["NO_PROXY"] = ",".join(no_proxy) if container.can_connect(): # first update configuration values pebble_layer = { "summary": "Candid Identity Service", "description": "Pebble config layer for candid", "services": { "candid": { "override": "merge", "summary": "Candid Identity Service", "command": "/root/candidsrv /root/config.yaml", "startup": "disabled", "environment": config_values, } }, "checks": { "candid-check": { "override": "replace", "period": "1m", "http": {"url": "http://localhost:8081/debug/status"}, } }, } container.add_layer("candid", pebble_layer, combine=True) # fetch the current plan current_plan = container.get_plan() # render the config.yaml args = current_plan.services.get("candid").environment config = self._render_template("config.yaml.tmpl", **args) if not container.exists(os.path.dirname(self._config_filename)): container.make_dir(os.path.dirname(self._config_filename)) container.push(self._config_filename, config) if self._ready(): if container.get_service("candid").is_running(): container.replan() else: container.start("candid") else: logger.info("workload container not ready - defering") event.defer() def _ready(self): container = self.unit.get_container(WORKLOAD_CONTAINER) if container.can_connect(): plan = container.get_plan() if plan.services.get("candid") is None: logger.error("waiting for service") self.unit.status = WaitingStatus("waiting for service") return False env_vars = plan.services.get("candid").environment for setting in REQUIRED_SETTINGS: if not env_vars.get(setting, ""): self.unit.status = BlockedStatus( "{} configuration value not set".format(setting), ) return False if container.get_service("candid").is_running(): self.unit.status = ActiveStatus("running") return True else: logger.error("cannot connect to workload container") self.unit.status = WaitingStatus("waiting for candid workload") return False def _render_template(self, name, **kwargs): """Load the template with the given name.""" loader = FileSystemLoader(os.path.join(self.charm_dir, "templates")) env = Environment(loader=loader) return env.get_template(name).render(**kwargs) def _generate_keypair(self, event): """Create a default keypair shared by all units in the application, if a keypair is not explicitely configured.""" container = self.unit.get_container(WORKLOAD_CONTAINER) if container.can_connect(): process = container.exec(["/root/bakery-keygen"]) try: stdout, _ = process.wait_output() return json.loads(stdout) except pebble.ExecError as e: logger.error( "error generating keypair %d. Stderr:", e.exit_code ) for line in e.stderr.splitlines(): logger.error(" %s", line) else: logger.info("workload container not ready - defering") event.defer() def _parse_identity_providers(self, idps): """parse the identity-providers configuration option.""" b64err = None try: idps = base64.b64decode(idps, validate=True) except binascii.Error as e: # Be tolerant of non-base64 values, to facilitate upgrades from # earlier charm versions. b64err = e try: return yaml.safe_load(idps) except yaml.YAMLError as e: msg = "error parsing identity-providers: {}".format(e) if b64err: msg += ", {}".format(b64err) raise IdentityProvidersParseError() if __name__ == "__main__": main(CandidOperatorCharm) golang-github-canonical-candid-1.12.3/charms/candid-k8s/templates/000077500000000000000000000000001457263123000246755ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid-k8s/templates/config.yaml.tmpl000066400000000000000000000020741457263123000300040ustar00rootroot00000000000000access-log: /root/logs/access.log auth-username: admin listen-address: :8081 max-mgo-sessions: 300 request-timeout: 2s resource-path: /root/www {%- if POSTGRESQL_DSN %} storage: type: postgres connection-string: {{ POSTGRESQL_DSN }} {%- else %} storage: type: memory {%- endif %} {%- if LOCATION %} location: {{ LOCATION }} {%- endif %} {%- if PRIVATE_KEY %} private-key: {{ PRIVATE_KEY }} {%- endif %} {%- if PUBLIC_KEY %} public-key: {{ PUBLIC_KEY }} {%- endif %} {%- if PRIVATE_ADDRESS%} private-addr: {{ PRIVATE_ADDRESS }} {%- else %} private-addr: localhost {%- endif %} {%- if ADMIN_AGENT_PUBLIC_KEY %} admin-agent-private-key: {{ ADMIN_AGENT_PUBLIC_KEY }} {%- endif %} {%- if MFA_RP_DISPLAY_NAME %} mfa-rp-display-name: {{ MFA_RP_DISPLAY_NAME }} {%- endif %} {%- if MFA_RP_ID %} mfa-rp-id: {{ MFA_RP_ID }} {%- endif %} {%- if MFA_RP_ORIGIN %} mfa-rp-origin: {{ MFA_RP_ORIGIN }} {%- endif %} {%- if IDENTITY_PROVIDERS %} identity-providers: {{ IDENTITY_PROVIDERS }} {%- endif %} {%- if ENABLE_EMAIL_LOGIN %} enable-email-login: {{ ENABLE_EMAIL_LOGIN }} {%- endif %} golang-github-canonical-candid-1.12.3/charms/candid-k8s/tests/000077500000000000000000000000001457263123000240415ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid-k8s/tests/__init__.py000066400000000000000000000000731457263123000261520ustar00rootroot00000000000000import ops.testing ops.testing.SIMULATE_CAN_CONNECT = True golang-github-canonical-candid-1.12.3/charms/candid-k8s/tests/test_charm.py000066400000000000000000000311111457263123000265410ustar00rootroot00000000000000# Copyright 2022 Canonical Ltd # See LICENSE file for licensing details. # # Learn more about testing at: https://juju.is/docs/sdk/testing import os import pathlib import shutil import tempfile import textwrap import unittest from unittest.mock import MagicMock, patch from charm import CandidOperatorCharm from ops.testing import Harness MINIMAL_CONFIG = { "admin-agent-public-key": "test-admin-public-key", "api-macaroon-timeout": "10m", "discharge-macaroon-timeout": "20m", "discharge-token-timeout": "30m", "identity-providers": """\ - type: static name: static description: Default identity provider require-mfa: true users: user1: name: User One email: user1@example.com password: password1 groups: - group1 - group3""", "location": "https://test-location", "private-key": "test-private-key", "public-key": "test-public-key", "rendezvous-timeout": "5m", } class test_process: def wait_output(self): return ( """{ "private": "generated-private-key", "public": "generated-public-key" }""", """{'a'}""", ) class TestCharm(unittest.TestCase): def setUp(self): self.maxDiff = None self.harness = Harness(CandidOperatorCharm) self.addCleanup(self.harness.cleanup) self.harness.disable_hooks() self.harness.add_oci_resource("candid-image") self.harness.begin() rel_id = self.harness.add_relation("ingress", "nginx-ingress") self.harness.add_relation_unit(rel_id, "nginx-ingress/0") self.tempdir = tempfile.TemporaryDirectory() self.addCleanup(self.tempdir.cleanup) shutil.copytree( os.path.join(self.harness.charm.charm_dir, "templates"), os.path.join(self.tempdir.name, "templates"), ) self.harness.charm.framework.charm_dir = pathlib.Path( self.tempdir.name ) self.harness.container_pebble_ready("candid") def test_on_pebble_ready(self): self.harness.update_config(MINIMAL_CONFIG) self.harness.update_config( {"private-key": "new-private-key", "public-key": "new-public-key"} ) container = self.harness.model.unit.get_container("candid") # Emit the pebble-ready event for jimm self.harness.charm.on.candid_pebble_ready.emit(container) # Check the that the plan was updated plan = self.harness.get_container_pebble_plan("candid") self.assertEqual( plan.to_dict(), { "services": { "candid": { "summary": "Candid Identity Service", "startup": "disabled", "override": "merge", "command": "/root/candidsrv /root/config.yaml", "environment": { "ADMIN_AGENT_PUBLIC_KEY": "test-admin-public-key", "API_MACAROON_TIMEOUT": "10m", "DISCHARGE_MACAROON_TIMEOUT": "20m", "DISCHARGE_TOKEN_TIMEOUT": "30m", "IDENTITY_PROVIDERS": """\ - type: static name: static description: Default identity provider require-mfa: true users: user1: name: User One email: user1@example.com password: password1 groups: - group1 - group3""", "LOCATION": "https://test-location", "LOGGING_CONFIG": "INFO", "PRIVATE_KEY": "new-private-key", "PUBLIC_KEY": "new-public-key", "RENDEZVOUS_TIMEOUT": "5m", }, } } }, ) config = container.pull("/root/config.yaml") self.assertEqual( textwrap.dedent( """\ access-log: /root/logs/access.log auth-username: admin listen-address: :8081 max-mgo-sessions: 300 request-timeout: 2s resource-path: /root/www storage: type: memory location: https://test-location private-key: new-private-key public-key: new-public-key private-addr: localhost admin-agent-private-key: test-admin-public-key identity-providers: - type: static name: static description: Default identity provider require-mfa: true users: user1: name: User One email: user1@example.com password: password1 groups: - group1 - group3""" ), config.read(), ) def test_on_config_changed(self): self.harness.update_config(MINIMAL_CONFIG) container = self.harness.model.unit.get_container("candid") self.harness.charm.on.candid_pebble_ready.emit(container) # Check the that the plan was updated plan = self.harness.get_container_pebble_plan("candid") self.assertEqual( plan.to_dict(), { "services": { "candid": { "summary": "Candid Identity Service", "startup": "disabled", "override": "merge", "command": "/root/candidsrv /root/config.yaml", "environment": { "ADMIN_AGENT_PUBLIC_KEY": "test-admin-public-key", "API_MACAROON_TIMEOUT": "10m", "DISCHARGE_MACAROON_TIMEOUT": "20m", "DISCHARGE_TOKEN_TIMEOUT": "30m", "IDENTITY_PROVIDERS": """\ - type: static name: static description: Default identity provider require-mfa: true users: user1: name: User One email: user1@example.com password: password1 groups: - group1 - group3""", "LOCATION": "https://test-location", "LOGGING_CONFIG": "INFO", "PRIVATE_KEY": "test-private-key", "PUBLIC_KEY": "test-public-key", "RENDEZVOUS_TIMEOUT": "5m", }, } } }, ) config = container.pull("/root/config.yaml") self.assertEqual( textwrap.dedent( """\ access-log: /root/logs/access.log auth-username: admin listen-address: :8081 max-mgo-sessions: 300 request-timeout: 2s resource-path: /root/www storage: type: memory location: https://test-location private-key: test-private-key public-key: test-public-key private-addr: localhost admin-agent-private-key: test-admin-public-key identity-providers: - type: static name: static description: Default identity provider require-mfa: true users: user1: name: User One email: user1@example.com password: password1 groups: - group1 - group3""" ), config.read(), ) @patch("ops.model.Container.exec") def test_on_leader_elected(self, exec): exec.return_value = test_process() self.harness.charm.db = MagicMock() self.harness.update_config( { "admin-agent-public-key": "test-admin-public-key", "api-macaroon-timeout": "10m", "discharge-macaroon-timeout": "20m", "discharge-token-timeout": "30m", "identity-providers": "test-identity-providers", "location": "https://test-location", "rendezvous-timeout": "5m", } ) rel_id = self.harness.add_relation("candid", "candid") self.harness.add_relation_unit(rel_id, "candid/1") self.harness.set_leader(True) self.harness.charm.on.leader_elected.emit() # Check the that the plan was updated plan = self.harness.get_container_pebble_plan("candid") self.assertEqual( plan.to_dict(), { "services": { "candid": { "summary": "Candid Identity Service", "startup": "disabled", "override": "merge", "command": "/root/candidsrv /root/config.yaml", "environment": { "ADMIN_AGENT_PUBLIC_KEY": "test-admin-public-key", "API_MACAROON_TIMEOUT": "10m", "DISCHARGE_MACAROON_TIMEOUT": "20m", "DISCHARGE_TOKEN_TIMEOUT": "30m", "IDENTITY_PROVIDERS": "test-identity-providers", "LOCATION": "https://test-location", "LOGGING_CONFIG": "INFO", "PRIVATE_KEY": "generated-private-key", "PUBLIC_KEY": "generated-public-key", "RENDEZVOUS_TIMEOUT": "5m", }, } } }, ) self.assertEqual( self.harness.get_relation_data(rel_id, "candid-k8s"), { "private-key": "generated-private-key", "public-key": "generated-public-key", }, ) def test_keys_from_relation_data(self): self.harness.update_config({"no-proxy": "192.168.0.1"}) rel_id = self.harness.add_relation("candid", "candid-k8s") self.harness.add_relation_unit(rel_id, "candid-k8s/1") self.harness.set_leader(False) self.harness.update_relation_data( rel_id, "candid-k8s", { "private-key": "generated-private-key", "public-key": "generated-public-key", }, ) self.harness.update_relation_data( rel_id, "candid-k8s/1", { "private-address": "192.168.0.2", }, ) container = self.harness.model.unit.get_container("candid") self.harness.charm.on.candid_pebble_ready.emit(container) self.harness.update_config({}) # Check the that the plan was updated plan = self.harness.get_container_pebble_plan("candid") self.assertEqual( plan.to_dict(), { "services": { "candid": { "summary": "Candid Identity Service", "startup": "disabled", "override": "merge", "command": "/root/candidsrv /root/config.yaml", "environment": { "API_MACAROON_TIMEOUT": "48h", "DISCHARGE_MACAROON_TIMEOUT": "48h", "DISCHARGE_TOKEN_TIMEOUT": "48h", "LOGGING_CONFIG": "INFO", "NO_PROXY": "192.168.0.1,192.168.0.2", "PRIVATE_KEY": "generated-private-key", "PUBLIC_KEY": "generated-public-key", "RENDEZVOUS_TIMEOUT": "10m", }, } } }, ) golang-github-canonical-candid-1.12.3/charms/candid/000077500000000000000000000000001457263123000221745ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid/.flake8000066400000000000000000000001611457263123000233450ustar00rootroot00000000000000[flake8] max-line-length = 99 ignore = W503 select: E,W,F,C,N exclude: venv .git build dist *.egg_info golang-github-canonical-candid-1.12.3/charms/candid/.gitignore000066400000000000000000000000671457263123000241670ustar00rootroot00000000000000venv/ build/ *.charm .coverage __pycache__/ *.py[cod] golang-github-canonical-candid-1.12.3/charms/candid/.jujuignore000066400000000000000000000000301457263123000243470ustar00rootroot00000000000000/venv *.py[cod] *.charm golang-github-canonical-candid-1.12.3/charms/candid/CONTRIBUTING.md000066400000000000000000000015361457263123000244320ustar00rootroot00000000000000# candid ## Developing Create and activate a virtualenv with the development requirements: virtualenv -p python3 venv source venv/bin/activate pip install -r requirements-dev.txt ## Code overview TEMPLATE-TODO: One of the most important things a consumer of your charm (or library) needs to know is what set of functionality it provides. Which categories does it fit into? Which events do you listen to? Which libraries do you consume? Which ones do you export and how are they used? ## Intended use case TEMPLATE-TODO: Why were these decisions made? What's the scope of your charm? ## Roadmap If this Charm doesn't fulfill all of the initial functionality you were hoping for or planning on, please add a Roadmap or TODO here ## Testing To run unit test run: ``` tox -e unit ``` To run integration tests run: ``` tox -e integration ```golang-github-canonical-candid-1.12.3/charms/candid/LICENSE000066400000000000000000000261361457263123000232110ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. golang-github-canonical-candid-1.12.3/charms/candid/README.md000066400000000000000000000003151457263123000234520ustar00rootroot00000000000000# Candid Identity service The Candid server provides a macaroon-based authentication service. The charm is designed to by in a model using haproxy as a frontend and storing data in a postgresql database. golang-github-canonical-candid-1.12.3/charms/candid/actions.yaml000066400000000000000000000000001457263123000245060ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid/build_resources.sh000077500000000000000000000001321457263123000257200ustar00rootroot00000000000000#!/bin/sh cp /home/alesstimec/Canonical/go/src/github.com/canonical/candid/candid.snap . golang-github-canonical-candid-1.12.3/charms/candid/charmcraft.yaml000066400000000000000000000003511457263123000251710ustar00rootroot00000000000000# Learn more about charmcraft.yaml configuration at: # https://juju.is/docs/sdk/charmcraft-config type: "charm" bases: - build-on: - name: "ubuntu" channel: "20.04" run-on: - name: "ubuntu" channel: "20.04" golang-github-canonical-candid-1.12.3/charms/candid/config.yaml000066400000000000000000000066651457263123000243420ustar00rootroot00000000000000options: admin-agent-public-key: type: string default: "" description: | Base64 encoded 256-bit Ed25519 public key for admin agent. api-macaroon-timeout: type: string default: "48h" description: | The maximum age an API macaroon can get before requiring re-authorization. discharge-macaroon-timeout: type: string default: "48h" description: | The maximum age a discharge macaroon can get before it becomes invalid. discharge-token-timeout: type: string default: "48h" description: | The maximum age a discharge token can get before it becomes invalid. enable-email-login: type: boolean default: false description: | Enable the "login with email address" functionality on the authentication required page. http-proxy: type: string default: "" description: Address of proxy to use for outgoing HTTP connections. identity-providers: type: string default: "" description: | This is a Base64 encoded YAML array containing the identity provider definition. See https://github.com/CanonicalLtd/candid/blob/master/docs/configuration.md#identity-providers-1 for a full list of possible options. location: type: string default: "" description: | Publicly accessible URL of the identity manager (defaults to public address of unit). logging-config: type: string default: INFO description: Loggo logging configuration string. no-proxy: type: string default: "" description: | List of addresses that should not use the proxy specified in http-proxy. If specified this should be a comma-separated list of addresses. private-key: type: string default: "" description: Base64 encoded 256-bit Ed25519 private key of the server. public-key: type: string default: "" description: | Base64 encoded 256-bit Ed25519 public key, this should match the private key. redirect-login-trusted-urls: type: string default: "" description: | List of URLs that are trusted to return to when using the redirect login flow. redirect-login-trusted-domains: type: string default: "" description: | Comma separated list of domains for which all redirect paths are trusted. If a domain starts with "*." that is taken to be a wildcard and will match all subdomains of the specified domain. rendezvous-timeout: type: string default: "10m" description: | Amount of time that an interactive authentication request can be active before it is forgotten. The value must be a time duration specified as a decimal number followed by a unit from ns, us, ms, s, m, h for the time units between nanosecond and hour. skip-location-for-cookie-paths: type: boolean default: false description: | If true, it leaves cookies' Path value absolute, instead of setting it relative to the path in the location config. mfa-rp-display-name: type: string default: "" description: | Display name for the multi-factor authentication relying party. mfa-rp-id: type: string default: "" description: | ID of the multi-factor authentication relying party (usually the FQDN for your site). mfa-rp-origin: type: string default: "" description: | Origin of the multi-factor authentication WebAuthn requests. golang-github-canonical-candid-1.12.3/charms/candid/lib/000077500000000000000000000000001457263123000227425ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid/lib/charms/000077500000000000000000000000001457263123000242175ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid/lib/charms/operator_libs_linux/000077500000000000000000000000001457263123000303025ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid/lib/charms/operator_libs_linux/v2/000077500000000000000000000000001457263123000306315ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid/lib/charms/operator_libs_linux/v2/snap.py000066400000000000000000001133331457263123000321500ustar00rootroot00000000000000# Copyright 2021 Canonical 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. """Representations of the system's Snaps, and abstractions around managing them. The `snap` module provides convenience methods for listing, installing, refreshing, and removing Snap packages, in addition to setting and getting configuration options for them. In the `snap` module, `SnapCache` creates a dict-like mapping of `Snap` objects at when instantiated. Installed snaps are fully populated, and available snaps are lazily-loaded upon request. This module relies on an installed and running `snapd` daemon to perform operations over the `snapd` HTTP API. `SnapCache` objects can be used to install or modify Snap packages by name in a manner similar to using the `snap` command from the commandline. An example of adding Juju to the system with `SnapCache` and setting a config value: ```python try: cache = snap.SnapCache() juju = cache["juju"] if not juju.present: juju.ensure(snap.SnapState.Latest, channel="beta") juju.set({"some.key": "value", "some.key2": "value2"}) except snap.SnapError as e: logger.error("An exception occurred when installing charmcraft. Reason: %s", e.message) ``` In addition, the `snap` module provides "bare" methods which can act on Snap packages as simple function calls. :meth:`add`, :meth:`remove`, and :meth:`ensure` are provided, as well as :meth:`add_local` for installing directly from a local `.snap` file. These return `Snap` objects. As an example of installing several Snaps and checking details: ```python try: nextcloud, charmcraft = snap.add(["nextcloud", "charmcraft"]) if nextcloud.get("mode") != "production": nextcloud.set({"mode": "production"}) except snap.SnapError as e: logger.error("An exception occurred when installing snaps. Reason: %s" % e.message) ``` """ import http.client import json import logging import os import re import socket import subprocess import sys import urllib.error import urllib.parse import urllib.request from collections.abc import Mapping from datetime import datetime, timedelta, timezone from enum import Enum from subprocess import CalledProcessError, CompletedProcess from typing import Any, Dict, Iterable, List, Optional, Union logger = logging.getLogger(__name__) # The unique Charmhub library identifier, never change it LIBID = "05394e5893f94f2d90feb7cbe6b633cd" # Increment this major API version when introducing breaking changes LIBAPI = 2 # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version LIBPATCH = 3 # Regex to locate 7-bit C1 ANSI sequences ansi_filter = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") def _cache_init(func): def inner(*args, **kwargs): if _Cache.cache is None: _Cache.cache = SnapCache() return func(*args, **kwargs) return inner # recursive hints seems to error out pytest JSONType = Union[Dict[str, Any], List[Any], str, int, float] class SnapService: """Data wrapper for snap services.""" def __init__( self, daemon: Optional[str] = None, daemon_scope: Optional[str] = None, enabled: bool = False, active: bool = False, activators: List[str] = [], **kwargs, ): self.daemon = daemon self.daemon_scope = kwargs.get("daemon-scope", None) or daemon_scope self.enabled = enabled self.active = active self.activators = activators def as_dict(self) -> Dict: """Return instance representation as dict.""" return { "daemon": self.daemon, "daemon_scope": self.daemon_scope, "enabled": self.enabled, "active": self.active, "activators": self.activators, } class MetaCache(type): """MetaCache class used for initialising the snap cache.""" @property def cache(cls) -> "SnapCache": """Property for returning the snap cache.""" return cls._cache @cache.setter def cache(cls, cache: "SnapCache") -> None: """Setter for the snap cache.""" cls._cache = cache def __getitem__(cls, name) -> "Snap": """Snap cache getter.""" return cls._cache[name] class _Cache(object, metaclass=MetaCache): _cache = None class Error(Exception): """Base class of most errors raised by this library.""" def __repr__(self): """Represent the Error class.""" return "<{}.{} {}>".format( type(self).__module__, type(self).__name__, self.args ) @property def name(self): """Return a string representation of the model plus class.""" return "<{}.{}>".format(type(self).__module__, type(self).__name__) @property def message(self): """Return the message passed as an argument.""" return self.args[0] class SnapAPIError(Error): """Raised when an HTTP API error occurs talking to the Snapd server.""" def __init__(self, body: Dict, code: int, status: str, message: str): super().__init__(message) # Makes str(e) return message self.body = body self.code = code self.status = status self._message = message def __repr__(self): """Represent the SnapAPIError class.""" return "APIError({!r}, {!r}, {!r}, {!r})".format( self.body, self.code, self.status, self._message ) class SnapState(Enum): """The state of a snap on the system or in the cache.""" Present = "present" Absent = "absent" Latest = "latest" Available = "available" class SnapError(Error): """Raised when there's an error running snap control commands.""" class SnapNotFoundError(Error): """Raised when a requested snap is not known to the system.""" class Snap(object): """Represents a snap package and its properties. `Snap` exposes the following properties about a snap: - name: the name of the snap - state: a `SnapState` representation of its install status - channel: "stable", "candidate", "beta", and "edge" are common - revision: a string representing the snap's revision - confinement: "classic" or "strict" """ def __init__( self, name, state: SnapState, channel: str, revision: str, confinement: str, apps: Optional[List[Dict[str, str]]] = None, cohort: Optional[str] = "", ) -> None: self._name = name self._state = state self._channel = channel self._revision = revision self._confinement = confinement self._cohort = cohort self._apps = apps or [] self._snap_client = SnapClient() def __eq__(self, other) -> bool: """Equality for comparison.""" return isinstance(other, self.__class__) and ( self._name, self._revision, ) == (other._name, other._revision) def __hash__(self): """Calculate a hash for this snap.""" return hash((self._name, self._revision)) def __repr__(self): """Represent the object such that it can be reconstructed.""" return "<{}.{}: {}>".format( self.__module__, self.__class__.__name__, self.__dict__ ) def __str__(self): """Represent the snap object as a string.""" return "<{}: {}-{}.{} -- {}>".format( self.__class__.__name__, self._name, self._revision, self._channel, str(self._state), ) def _snap(self, command: str, optargs: Optional[Iterable[str]] = None) -> str: """Perform a snap operation. Args: command: the snap command to execute optargs: an (optional) list of additional arguments to pass, commonly confinement or channel Raises: SnapError if there is a problem encountered """ optargs = optargs or [] args = ["snap", command, self._name, *optargs] try: return subprocess.check_output(args, universal_newlines=True) except CalledProcessError as e: raise SnapError( "Snap: {!r}; command {!r} failed with output = {!r}".format( self._name, args, e.output ) ) def _snap_daemons( self, command: List[str], services: Optional[List[str]] = None, ) -> CompletedProcess: """Perform snap app commands. Args: command: the snap command to execute services: the snap service to execute command on Raises: SnapError if there is a problem encountered """ if services: # an attempt to keep the command constrained to the snap instance's services services = ["{}.{}".format(self._name, service) for service in services] else: services = [self._name] args = ["snap", *command, *services] try: return subprocess.run( args, universal_newlines=True, check=True, capture_output=True ) except CalledProcessError as e: raise SnapError( "Could not {} for snap [{}]: {}".format(args, self._name, e.stderr) ) def get(self, key: Optional[str], *, typed: bool = False) -> Any: """Fetch snap configuration values. Args: key: the key to retrieve. Default to retrieve all values for typed=True. typed: set to True to retrieve typed values (set with typed=True). Default is to return a string. """ if typed: config = json.loads(self._snap("get", ["-d", key])) if key: return config.get(key) return config if not key: raise TypeError("Key must be provided when typed=False") return self._snap("get", [key]).strip() def set(self, config: Dict[str, Any], *, typed: bool = False) -> str: """Set a snap configuration value. Args: config: a dictionary containing keys and values specifying the config to set. typed: set to True to convert all values in the config into typed values while configuring the snap (set with typed=True). Default is not to convert. """ if typed: kv = [f"{key}={json.dumps(val)}" for key, val in config.items()] return self._snap("set", ["-t"] + kv) return self._snap("set", [f"{key}={val}" for key, val in config.items()]) def unset(self, key) -> str: """Unset a snap configuration value. Args: key: the key to unset """ return self._snap("unset", [key]) def start( self, services: Optional[List[str]] = None, enable: Optional[bool] = False ) -> None: """Start a snap's services. Args: services (list): (optional) list of individual snap services to start (otherwise all) enable (bool): (optional) flag to enable snap services on start. Default `false` """ args = ["start", "--enable"] if enable else ["start"] self._snap_daemons(args, services) def stop( self, services: Optional[List[str]] = None, disable: Optional[bool] = False ) -> None: """Stop a snap's services. Args: services (list): (optional) list of individual snap services to stop (otherwise all) disable (bool): (optional) flag to disable snap services on stop. Default `False` """ args = ["stop", "--disable"] if disable else ["stop"] self._snap_daemons(args, services) def logs( self, services: Optional[List[str]] = None, num_lines: Optional[int] = 10 ) -> str: """Fetch a snap services' logs. Args: services (list): (optional) list of individual snap services to show logs from (otherwise all) num_lines (int): (optional) integer number of log lines to return. Default `10` """ args = ["logs", "-n={}".format(num_lines)] if num_lines else ["logs"] return self._snap_daemons(args, services).stdout def connect( self, plug: str, service: Optional[str] = None, slot: Optional[str] = None ) -> None: """Connect a plug to a slot. Args: plug (str): the plug to connect service (str): (optional) the snap service name to plug into slot (str): (optional) the snap service slot to plug in to Raises: SnapError if there is a problem encountered """ command = ["connect", "{}:{}".format(self._name, plug)] if service and slot: command = command + ["{}:{}".format(service, slot)] elif slot: command = command + [slot] args = ["snap", *command] try: subprocess.run( args, universal_newlines=True, check=True, capture_output=True ) except CalledProcessError as e: raise SnapError( "Could not {} for snap [{}]: {}".format(args, self._name, e.stderr) ) def hold(self, duration: Optional[timedelta] = None) -> None: """Add a refresh hold to a snap. Args: duration: duration for the hold, or None (the default) to hold this snap indefinitely. """ hold_str = "forever" if duration is not None: seconds = round(duration.total_seconds()) hold_str = f"{seconds}s" self._snap("refresh", [f"--hold={hold_str}"]) def unhold(self) -> None: """Remove the refresh hold of a snap.""" self._snap("refresh", ["--unhold"]) def alias(self, application: str, alias: Optional[str] = None) -> None: """Create an alias for a given application. Args: application: application to get an alias. alias: (optional) name of the alias; if not provided, the application name is used. """ if alias is None: alias = application args = ["snap", "alias", f"{self.name}.{application}", alias] try: subprocess.check_output(args, universal_newlines=True) except CalledProcessError as e: raise SnapError( "Snap: {!r}; command {!r} failed with output = {!r}".format( self._name, args, e.output ) ) def restart( self, services: Optional[List[str]] = None, reload: Optional[bool] = False ) -> None: """Restarts a snap's services. Args: services (list): (optional) list of individual snap services to restart. (otherwise all) reload (bool): (optional) flag to use the service reload command, if available. Default `False` """ args = ["restart", "--reload"] if reload else ["restart"] self._snap_daemons(args, services) def _install( self, channel: Optional[str] = "", cohort: Optional[str] = "", revision: Optional[str] = None, ) -> None: """Add a snap to the system. Args: channel: the channel to install from cohort: optional, the key of a cohort that this snap belongs to revision: optional, the revision of the snap to install """ cohort = cohort or self._cohort args = [] if self.confinement == "classic": args.append("--classic") if channel: args.append('--channel="{}"'.format(channel)) if revision: args.append('--revision="{}"'.format(revision)) if cohort: args.append('--cohort="{}"'.format(cohort)) self._snap("install", args) def _refresh( self, channel: Optional[str] = "", cohort: Optional[str] = "", revision: Optional[str] = None, leave_cohort: Optional[bool] = False, ) -> None: """Refresh a snap. Args: channel: the channel to install from cohort: optionally, specify a cohort. revision: optionally, specify the revision of the snap to refresh leave_cohort: leave the current cohort. """ args = [] if channel: args.append('--channel="{}"'.format(channel)) if revision: args.append('--revision="{}"'.format(revision)) if not cohort: cohort = self._cohort if leave_cohort: self._cohort = "" args.append("--leave-cohort") elif cohort: args.append('--cohort="{}"'.format(cohort)) self._snap("refresh", args) def _remove(self) -> str: """Remove a snap from the system.""" return self._snap("remove") @property def name(self) -> str: """Returns the name of the snap.""" return self._name def ensure( self, state: SnapState, classic: Optional[bool] = False, channel: Optional[str] = "", cohort: Optional[str] = "", revision: Optional[str] = None, ): """Ensure that a snap is in a given state. Args: state: a `SnapState` to reconcile to. classic: an (Optional) boolean indicating whether classic confinement should be used channel: the channel to install from cohort: optional. Specify the key of a snap cohort. revision: optional. the revision of the snap to install/refresh While both channel and revision could be specified, the underlying snap install/refresh command will determine which one takes precedence (revision at this time) Raises: SnapError if an error is encountered """ self._confinement = ( "classic" if classic or self._confinement == "classic" else "" ) if state not in (SnapState.Present, SnapState.Latest): # We are attempting to remove this snap. if self._state in (SnapState.Present, SnapState.Latest): # The snap is installed, so we run _remove. self._remove() else: # The snap is not installed -- no need to do anything. pass else: # We are installing or refreshing a snap. if self._state not in (SnapState.Present, SnapState.Latest): # The snap is not installed, so we install it. self._install(channel, cohort, revision) else: # The snap is installed, but we are changing it (e.g., switching channels). self._refresh(channel, cohort, revision) self._update_snap_apps() self._state = state def _update_snap_apps(self) -> None: """Update a snap's apps after snap changes state.""" try: self._apps = self._snap_client.get_installed_snap_apps(self._name) except SnapAPIError: logger.debug("Unable to retrieve snap apps for {}".format(self._name)) self._apps = [] @property def present(self) -> bool: """Report whether or not a snap is present.""" return self._state in (SnapState.Present, SnapState.Latest) @property def latest(self) -> bool: """Report whether the snap is the most recent version.""" return self._state is SnapState.Latest @property def state(self) -> SnapState: """Report the current snap state.""" return self._state @state.setter def state(self, state: SnapState) -> None: """Set the snap state to a given value. Args: state: a `SnapState` to reconcile the snap to. Raises: SnapError if an error is encountered """ if self._state is not state: self.ensure(state) self._state = state @property def revision(self) -> str: """Returns the revision for a snap.""" return self._revision @property def channel(self) -> str: """Returns the channel for a snap.""" return self._channel @property def confinement(self) -> str: """Returns the confinement for a snap.""" return self._confinement @property def apps(self) -> List: """Returns (if any) the installed apps of the snap.""" self._update_snap_apps() return self._apps @property def services(self) -> Dict: """Returns (if any) the installed services of the snap.""" self._update_snap_apps() services = {} for app in self._apps: if "daemon" in app: services[app["name"]] = SnapService(**app).as_dict() return services @property def held(self) -> bool: """Report whether the snap has a hold.""" info = self._snap("info") return "hold:" in info class _UnixSocketConnection(http.client.HTTPConnection): """Implementation of HTTPConnection that connects to a named Unix socket.""" def __init__(self, host, timeout=None, socket_path=None): if timeout is None: super().__init__(host) else: super().__init__(host, timeout=timeout) self.socket_path = socket_path def connect(self): """Override connect to use Unix socket (instead of TCP socket).""" if not hasattr(socket, "AF_UNIX"): raise NotImplementedError( "Unix sockets not supported on {}".format(sys.platform) ) self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.connect(self.socket_path) if self.timeout is not None: self.sock.settimeout(self.timeout) class _UnixSocketHandler(urllib.request.AbstractHTTPHandler): """Implementation of HTTPHandler that uses a named Unix socket.""" def __init__(self, socket_path: str): super().__init__() self.socket_path = socket_path def http_open(self, req) -> http.client.HTTPResponse: """Override http_open to use a Unix socket connection (instead of TCP).""" return self.do_open(_UnixSocketConnection, req, socket_path=self.socket_path) class SnapClient: """Snapd API client to talk to HTTP over UNIX sockets. In order to avoid shelling out and/or involving sudo in calling the snapd API, use a wrapper based on the Pebble Client, trimmed down to only the utility methods needed for talking to snapd. """ def __init__( self, socket_path: str = "/run/snapd.socket", opener: Optional[urllib.request.OpenerDirector] = None, base_url: str = "http://localhost/v2/", timeout: float = 30.0, ): """Initialize a client instance. Args: socket_path: a path to the socket on the filesystem. Defaults to /run/snap/snapd.socket opener: specifies an opener for unix socket, if unspecified a default is used base_url: base url for making requests to the snap client. Defaults to http://localhost/v2/ timeout: timeout in seconds to use when making requests to the API. Default is 30.0s. """ if opener is None: opener = self._get_default_opener(socket_path) self.opener = opener self.base_url = base_url self.timeout = timeout @classmethod def _get_default_opener(cls, socket_path): """Build the default opener to use for requests (HTTP over Unix socket).""" opener = urllib.request.OpenerDirector() opener.add_handler(_UnixSocketHandler(socket_path)) opener.add_handler(urllib.request.HTTPDefaultErrorHandler()) opener.add_handler(urllib.request.HTTPRedirectHandler()) opener.add_handler(urllib.request.HTTPErrorProcessor()) return opener def _request( self, method: str, path: str, query: Dict = None, body: Dict = None, ) -> JSONType: """Make a JSON request to the Snapd server with the given HTTP method and path. If query dict is provided, it is encoded and appended as a query string to the URL. If body dict is provided, it is serialied as JSON and used as the HTTP body (with Content-Type: "application/json"). The resulting body is decoded from JSON. """ headers = {"Accept": "application/json"} data = None if body is not None: data = json.dumps(body).encode("utf-8") headers["Content-Type"] = "application/json" response = self._request_raw(method, path, query, headers, data) return json.loads(response.read().decode())["result"] def _request_raw( self, method: str, path: str, query: Dict = None, headers: Dict = None, data: bytes = None, ) -> http.client.HTTPResponse: """Make a request to the Snapd server; return the raw HTTPResponse object.""" url = self.base_url + path if query: url = url + "?" + urllib.parse.urlencode(query) if headers is None: headers = {} request = urllib.request.Request(url, method=method, data=data, headers=headers) try: response = self.opener.open(request, timeout=self.timeout) except urllib.error.HTTPError as e: code = e.code status = e.reason message = "" try: body = json.loads(e.read().decode())["result"] except (IOError, ValueError, KeyError) as e2: # Will only happen on read error or if Pebble sends invalid JSON. body = {} message = "{} - {}".format(type(e2).__name__, e2) raise SnapAPIError(body, code, status, message) except urllib.error.URLError as e: raise SnapAPIError({}, 500, "Not found", e.reason) return response def get_installed_snaps(self) -> Dict: """Get information about currently installed snaps.""" return self._request("GET", "snaps") def get_snap_information(self, name: str) -> Dict: """Query the snap server for information about single snap.""" return self._request("GET", "find", {"name": name})[0] def get_installed_snap_apps(self, name: str) -> List: """Query the snap server for apps belonging to a named, currently installed snap.""" return self._request("GET", "apps", {"names": name, "select": "service"}) class SnapCache(Mapping): """An abstraction to represent installed/available packages. When instantiated, `SnapCache` iterates through the list of installed snaps using the `snapd` HTTP API, and a list of available snaps by reading the filesystem to populate the cache. Information about available snaps is lazily-loaded from the `snapd` API when requested. """ def __init__(self): if not self.snapd_installed: raise SnapError("snapd is not installed or not in /usr/bin") from None self._snap_client = SnapClient() self._snap_map = {} if self.snapd_installed: self._load_available_snaps() self._load_installed_snaps() def __contains__(self, key: str) -> bool: """Check if a given snap is in the cache.""" return key in self._snap_map def __len__(self) -> int: """Report number of items in the snap cache.""" return len(self._snap_map) def __iter__(self) -> Iterable["Snap"]: """Provide iterator for the snap cache.""" return iter(self._snap_map.values()) def __getitem__(self, snap_name: str) -> Snap: """Return either the installed version or latest version for a given snap.""" snap = self._snap_map.get(snap_name, None) if snap is None: # The snapd cache file may not have existed when _snap_map was # populated. This is normal. try: self._snap_map[snap_name] = self._load_info(snap_name) except SnapAPIError: raise SnapNotFoundError("Snap '{}' not found!".format(snap_name)) return self._snap_map[snap_name] @property def snapd_installed(self) -> bool: """Check whether snapd has been installled on the system.""" return os.path.isfile("/usr/bin/snap") def _load_available_snaps(self) -> None: """Load the list of available snaps from disk. Leave them empty and lazily load later if asked for. """ if not os.path.isfile("/var/cache/snapd/names"): # The snap catalog may not be populated yet; this is normal. # snapd updates the cache infrequently and the cache file may not # currently exist. return with open("/var/cache/snapd/names", "r") as f: for line in f: if line.strip(): self._snap_map[line.strip()] = None def _load_installed_snaps(self) -> None: """Load the installed snaps into the dict.""" installed = self._snap_client.get_installed_snaps() for i in installed: snap = Snap( name=i["name"], state=SnapState.Latest, channel=i["channel"], revision=i["revision"], confinement=i["confinement"], apps=i.get("apps", None), ) self._snap_map[snap.name] = snap def _load_info(self, name) -> Snap: """Load info for snaps which are not installed if requested. Args: name: a string representing the name of the snap """ info = self._snap_client.get_snap_information(name) return Snap( name=info["name"], state=SnapState.Available, channel=info["channel"], revision=info["revision"], confinement=info["confinement"], apps=None, ) @_cache_init def add( snap_names: Union[str, List[str]], state: Union[str, SnapState] = SnapState.Latest, channel: Optional[str] = "", classic: Optional[bool] = False, cohort: Optional[str] = "", revision: Optional[str] = None, ) -> Union[Snap, List[Snap]]: """Add a snap to the system. Args: snap_names: the name or names of the snaps to install state: a string or `SnapState` representation of the desired state, one of [`Present` or `Latest`] channel: an (Optional) channel as a string. Defaults to 'latest' classic: an (Optional) boolean specifying whether it should be added with classic confinement. Default `False` cohort: an (Optional) string specifying the snap cohort to use revision: an (Optional) string specifying the snap revision to use Raises: SnapError if some snaps failed to install or were not found. """ if not channel and not revision: channel = "latest" snap_names = [snap_names] if isinstance(snap_names, str) else snap_names if not snap_names: raise TypeError("Expected at least one snap to add, received zero!") if isinstance(state, str): state = SnapState(state) return _wrap_snap_operations(snap_names, state, channel, classic, cohort, revision) @_cache_init def remove(snap_names: Union[str, List[str]]) -> Union[Snap, List[Snap]]: """Remove specified snap(s) from the system. Args: snap_names: the name or names of the snaps to install Raises: SnapError if some snaps failed to install. """ snap_names = [snap_names] if isinstance(snap_names, str) else snap_names if not snap_names: raise TypeError("Expected at least one snap to add, received zero!") return _wrap_snap_operations(snap_names, SnapState.Absent, "", False) @_cache_init def ensure( snap_names: Union[str, List[str]], state: str, channel: Optional[str] = "", classic: Optional[bool] = False, cohort: Optional[str] = "", revision: Optional[int] = None, ) -> Union[Snap, List[Snap]]: """Ensure specified snaps are in a given state on the system. Args: snap_names: the name(s) of the snaps to operate on state: a string representation of the desired state, from `SnapState` channel: an (Optional) channel as a string. Defaults to 'latest' classic: an (Optional) boolean specifying whether it should be added with classic confinement. Default `False` cohort: an (Optional) string specifying the snap cohort to use revision: an (Optional) integer specifying the snap revision to use When both channel and revision are specified, the underlying snap install/refresh command will determine the precedence (revision at the time of adding this) Raises: SnapError if the snap is not in the cache. """ if not revision and not channel: channel = "latest" if state in ("present", "latest") or revision: return add(snap_names, SnapState(state), channel, classic, cohort, revision) else: return remove(snap_names) def _wrap_snap_operations( snap_names: List[str], state: SnapState, channel: str, classic: bool, cohort: Optional[str] = "", revision: Optional[str] = None, ) -> Union[Snap, List[Snap]]: """Wrap common operations for bare commands.""" snaps = {"success": [], "failed": []} op = "remove" if state is SnapState.Absent else "install or refresh" for s in snap_names: try: snap = _Cache[s] if state is SnapState.Absent: snap.ensure(state=SnapState.Absent) else: snap.ensure( state=state, classic=classic, channel=channel, cohort=cohort, revision=revision, ) snaps["success"].append(snap) except SnapError as e: logger.warning("Failed to {} snap {}: {}!".format(op, s, e.message)) snaps["failed"].append(s) except SnapNotFoundError: logger.warning("Snap '{}' not found in cache!".format(s)) snaps["failed"].append(s) if len(snaps["failed"]): raise SnapError( "Failed to install or refresh snap(s): {}".format( ", ".join(list(snaps["failed"])) ) ) return snaps["success"] if len(snaps["success"]) > 1 else snaps["success"][0] def install_local( filename: str, classic: Optional[bool] = False, dangerous: Optional[bool] = False ) -> Snap: """Perform a snap operation. Args: filename: the path to a local .snap file to install classic: whether to use classic confinement dangerous: whether --dangerous should be passed to install snaps without a signature Raises: SnapError if there is a problem encountered """ args = [ "snap", "install", filename, ] if classic: args.append("--classic") if dangerous: args.append("--dangerous") try: result = subprocess.check_output(args, universal_newlines=True).splitlines()[-1] snap_name, _ = result.split(" ", 1) snap_name = ansi_filter.sub("", snap_name) c = SnapCache() try: return c[snap_name] except SnapAPIError as e: logger.error( "Could not find snap {} when querying Snapd socket: {}".format( snap_name, e.body ) ) raise SnapError("Failed to find snap {} in Snap cache".format(snap_name)) except CalledProcessError as e: raise SnapError("Could not install snap {}: {}".format(filename, e.output)) def _system_set(config_item: str, value: str) -> None: """Set system snapd config values. Args: config_item: name of snap system setting. E.g. 'refresh.hold' value: value to assign """ args = ["snap", "set", "system", "{}={}".format(config_item, value)] try: subprocess.check_call(args, universal_newlines=True) except CalledProcessError: raise SnapError( "Failed setting system config '{}' to '{}'".format(config_item, value) ) def hold_refresh(days: int = 90, forever: bool = False) -> bool: """Set the system-wide snap refresh hold. Args: days: number of days to hold system refreshes for. Maximum 90. Set to zero to remove hold. forever: if True, will set a hold forever. """ if not isinstance(forever, bool): raise TypeError("forever must be a bool") if not isinstance(days, int): raise TypeError("days must be an int") if forever: _system_set("refresh.hold", "forever") logger.info("Set system-wide snap refresh hold to: forever") elif days == 0: _system_set("refresh.hold", "") logger.info("Removed system-wide snap refresh hold") else: # Currently the snap daemon can only hold for a maximum of 90 days if not 1 <= days <= 90: raise ValueError("days must be between 1 and 90") # Add the number of days to current time target_date = datetime.now(timezone.utc).astimezone() + timedelta(days=days) # Format for the correct datetime format hold_date = target_date.strftime("%Y-%m-%dT%H:%M:%S%z") # Python dumps the offset in format '+0100', we need '+01:00' hold_date = "{0}:{1}".format(hold_date[:-2], hold_date[-2:]) # Actually set the hold date _system_set("refresh.hold", hold_date) logger.info("Set system-wide snap refresh hold to: %s", hold_date) golang-github-canonical-candid-1.12.3/charms/candid/metadata.yaml000066400000000000000000000007401457263123000246410ustar00rootroot00000000000000name: candid display-name: candid summary: Candid identity server. maintainer: JAAS Developers description: | Candid macaroon-based authentication service. tags: - web_server - authenticator - authentication - identity peers: candid: interface: candid provides: website: interface: http requires: postgres: interface: pgsql resources: candid: type: file filename: candid.snap description: "Candid snap file"golang-github-canonical-candid-1.12.3/charms/candid/pytest.ini000066400000000000000000000002671457263123000242320ustar00rootroot00000000000000[pytest] log_cli = 1 log_cli_level = INFO log_cli_format = %(asctime)s [%(levelname)s] %(message)s (%(filename)s:%(lineno)s) log_cli_date_format=%Y-%m-%d %H:%M:%S asyncio_mode = auto golang-github-canonical-candid-1.12.3/charms/candid/requirements-dev.txt000066400000000000000000000000441457263123000262320ustar00rootroot00000000000000-r requirements.txt coverage flake8 golang-github-canonical-candid-1.12.3/charms/candid/requirements-test.txt000066400000000000000000000000441457263123000264330ustar00rootroot00000000000000pytest-operator pytest-asyncio juju golang-github-canonical-candid-1.12.3/charms/candid/requirements.txt000066400000000000000000000000321457263123000254530ustar00rootroot00000000000000ops >= 1.4.0 ops-lib-pgsqlgolang-github-canonical-candid-1.12.3/charms/candid/run_tests000077500000000000000000000005401457263123000241470ustar00rootroot00000000000000#!/bin/sh -e # Copyright 2022 Ales Stimec # See LICENSE file for licensing details. if [ -z "$VIRTUAL_ENV" -a -d venv/ ]; then . venv/bin/activate fi if [ -z "$PYTHONPATH" ]; then export PYTHONPATH="lib:src" else export PYTHONPATH="lib:src:$PYTHONPATH" fi flake8 coverage run --branch --source=src -m unittest -v "$@" coverage report -m golang-github-canonical-candid-1.12.3/charms/candid/src/000077500000000000000000000000001457263123000227635ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid/src/charm.py000077500000000000000000000257201457263123000244400ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright 2022 Ales Stimec # See LICENSE file for licensing details. # # Learn more at: https://juju.is/docs/sdk """Charm the service. Refer to the following post for a quick-start guide that will help you develop a new k8s charm using the Operator Framework: https://discourse.charmhub.io/t/4208 """ import hashlib import logging from collections.abc import MutableMapping import pgsql from charms.operator_libs_linux.v2.snap import Snap, SnapCache, install_local, remove from ops.charm import CharmBase from ops.main import main from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus from state import State, requires_state logger = logging.getLogger(__name__) SNAP_NAME = "candid" REQUIRED_SETTINGS = [ "admin-agent-public-key", "api-macaroon-timeout", "discharge-macaroon-timeout", "discharge-token-timeout", "identity-providers", "location", "private-key", "public-key", "rendezvous-timeout", ] class CandidCharm(CharmBase): """Charm the service.""" @property def snap(self) -> Snap: """Retrieves snap from the snap cache""" return SnapCache().get(SNAP_NAME) @property def snap_running(self): """Reports if the 'candidsrv' snap daemon is running.""" return self.snap.services["candidsrv"]["active"] def __init__(self, *args): super().__init__(*args) # Hooks self.framework.observe(self.on.install, self._install) self.framework.observe(self.on.start, self._start) self.framework.observe(self.on.upgrade_charm, self._on_upgrade_charm) self.framework.observe(self.on.config_changed, self._config_changed) self.framework.observe( self.on.candid_relation_changed, self._on_candid_relation_changed ) self.framework.observe( self.on.candid_relation_departed, self._on_candid_relation_departed ) self.framework.observe( self.on.website_relation_joined, self._on_website_relation_joined ) # Database self.db = pgsql.PostgreSQLClient(self, "postgres") self.framework.observe( self.db.on.database_relation_joined, self._on_database_relation_joined, ) self.framework.observe(self.db.on.master_changed, self._on_master_changed) self._state = State(self.unit, lambda: self.model.get_relation("candid")) ################### # LIFECYCLE HOOKS # ################### def _on_candid_relation_changed(self, event): self._update_config_and_restart(event) def _on_candid_relation_departed(self, event): self._update_config_and_restart(event) def _install(self, event): """ Install candid snap """ logger.info("running install snap") self._install_snap(event) if self.snap.present: self.set_status_and_log("Snap installed", WaitingStatus) def _start(self, event): """ Starts candidsrv service """ if not self._check_config(event): event.defer() return self.set_status_and_log("Starting candid", WaitingStatus) self.snap.start(["candidsrv"]) if self.snap_running: self.set_status_and_log("Ready", ActiveStatus) def _on_upgrade_charm(self, event): self._install(event) self._start(event) def _config_changed(self, event): """ Updates snap internal configuration. """ self._update_config_and_restart(event) @requires_state def _update_config_and_restart(self, event): if not self._check_config(event): if self.snap_running: self.snap.stop(services=["candidsrv"]) return config_values = {key: value for key, value in self.config.items() if value} config_values["storage"] = { "type": "postgres", "connection-string": self._state.db_uri, } relation = self.model.get_relation("candid") if relation: peers = [] for unit, data in relation.data.items(): addr = data.get("private-address", "") if addr: peers.append(addr) if len(peers) > 0: config_values["no-proxy"] = ",".join(peers) logging.debug("setting config values {}".format(config_values)) config_values = flatten_dict(config_values) config_values = { "candid.{}".format(key): value for key, value in config_values.items() if value } try: self.snap.set(config_values) except Exception as e: logging.error("error setting snap configuration values: {}".format(e)) self.set_status_and_log("Restarting.", WaitingStatus) if self.snap_running: self.snap.restart(["candidsrv"]) else: self.snap.start(services=["candidsrv"]) if self.snap_running: self.set_status_and_log("Ready", ActiveStatus) @requires_state def _check_config(self, event) -> bool: """ Checks if required config is set and relations added. """ if not self.snap.present: # TODO: We need error status, how? self.set_status_and_log("Snap not installed", MaintenanceStatus) return False if self._state.db_uri is None: self.set_status_and_log( "Waiting for postgres connection string", WaitingStatus ) return False for setting in REQUIRED_SETTINGS: if not self.config.get(setting, ""): self.unit.status = BlockedStatus( "{} configuration value not set".format(setting), ) return False return True def set_status_and_log(self, msg, status) -> None: """ A simple wrapper to log and set unit status simultaneously. """ logging.info(msg) self.unit.status = status(msg) #################### # WEBSITE RELATION # #################### def _on_website_relation_joined(self, event): """Connect a website relation.""" event.relation.data[self.unit]["port"] = "8081" ##################### # DATABASE RELATION # ##################### def _on_database_relation_joined( self, event: pgsql.DatabaseRelationJoinedEvent ) -> None: """ Handles determining if the database has finished setup, once setup is complete a master/standby may join / change in consequent events. """ logging.info("(postgresql) RELATION_JOINED event fired.") if self.model.unit.is_leader(): event.database = "candid" elif event.database != "candid": event.defer() @requires_state def _on_master_changed(self, event: pgsql.MasterChangedEvent) -> None: """ Handles master units of postgres joining / changing. The internal snap configuration is updated to reflect this. """ logging.info("(postgresql) MASTER_CHANGED event fired.") if event.database != "candid": logging.info("Database setup not complete yet, returning.") return self.set_status_and_log("Updating database configuration...", WaitingStatus) if not event.master or not event.master.uri: logging.debug("removing database connection string") del self._state.db_uri else: uri = event.master.uri if not uri: uri = "" logging.info("database uri {}".format(uri)) self._state.db_uri = None if event.master is None else event.master.uri self._update_config_and_restart(event) ############# # UTILITIES # ############# @requires_state def _install_snap(self, _) -> None: """ Installs the Candid snap. """ resource_path = self.model.resources.fetch("candid") logger.info("resource path {}".format(resource_path)) resource_hash = file_hash(resource_path) resource_changed = False if self._state.resource_hash: if self._state.resource_hash != resource_hash: resource_changed = True else: resource_changed = True logger.info("resource changed {}".format(resource_changed)) if not resource_changed: logger.info("resource has not changed") return if self.snap.present: logger.info("removing existing snap") remove(SNAP_NAME) try: self.set_status_and_log("Installing snap", WaitingStatus) logger.info("installing resource {}".format(resource_path)) install_local(resource_path, classic=True, dangerous=True) self._state.resource_hash = resource_hash except Exception as e: logger.info("failed to install snap {}".format(e)) self.set_status_and_log("Could not install snap", BlockedStatus) # flatten_dict copied from ops.model def flatten_dict(input: dict, parent_key: str = None, output: dict = None) -> dict: """Turn a nested dictionary into a flattened dictionary, using '.' as a key separator. This is used to allow nested dictionaries to be translated into the dotted format required by the Juju `action-set` hook tool in order to set nested data on an action. Additionally, this method performs some validation on keys to ensure they only use permitted characters. Example:: >>> test_dict = {'a': {'b': 1, 'c': 2}} >>> _format_action_result_dict(test_dict) {'a.b': 1, 'a.c': 2} Arguments: input: The dictionary to flatten parent_key: The string to prepend to dictionary's keys output: The current dictionary to be returned, which may or may not yet be completely flat Returns: A flattened dictionary with validated keys Raises: ValueError: if the dict is passed with a mix of dotted/non-dotted keys that expand out to result in duplicate keys. For example: {'a': {'b': 1}, 'a.b': 2}. Also raised if a dict is passed with a key that fails to meet the format requirements. """ if output is None: output = {} for key, value in input.items(): if parent_key: key = "{}.{}".format(parent_key, key) if isinstance(value, MutableMapping): output = flatten_dict(value, key, output) elif key in output: raise ValueError( "duplicate key detected in dictionary passed to 'action-set': {!r}".format( key ) ) else: output[key] = value return output def file_hash(filename: str) -> str: with open(filename, "rb") as f: bytes = f.read() # read entire file as bytes return hashlib.sha256(bytes).hexdigest() if __name__ == "__main__": main(CandidCharm) golang-github-canonical-candid-1.12.3/charms/candid/src/state.py000066400000000000000000000045571457263123000244700ustar00rootroot00000000000000# Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. """Manager for handling charm state.""" import functools import json def requires_state_setter(func): @functools.wraps(func) def wrapper(self, event): if self.unit.is_leader() and self._state.is_ready(): return func(self, event) else: return return wrapper def requires_state(func): @functools.wraps(func) def wrapper(self, event): if self._state.is_ready(): return func(self, event) else: event.defer() return return wrapper class State: """A magic state that uses a relation as the data store. The get_relation callable is used to retrieve the relation. As relation data values must be strings, all values are JSON encoded. """ def __init__(self, app, get_relation): """Construct. Args: app: workload application get_relation: get peer relation method """ # Use __dict__ to avoid calling __setattr__ and subsequent infinite recursion. self.__dict__["_app"] = app self.__dict__["_get_relation"] = get_relation def __setattr__(self, name, value): """Set a value in the store with the given name. Args: name: name of value to set in store. value: value to set in store. """ v = json.dumps(value) self._get_relation().data[self._app].update({name: v}) def __getattr__(self, name): """Get from the store the value with the given name, or None. Args: name: name of value to get from store. Returns: value from store with given name. """ v = self._get_relation().data[self._app].get(name, "null") return json.loads(v) def __delattr__(self, name): """Delete the value with the given name from the store, if it exists. Args: name: name of value to delete from store. Returns: deleted value from store. """ return self._get_relation().data[self._app].pop(name, None) def is_ready(self): """Report whether the relation is ready to be used. Returns: A boolean representing whether the relation is ready to be used or not. """ return bool(self._get_relation()) golang-github-canonical-candid-1.12.3/charms/candid/tests/000077500000000000000000000000001457263123000233365ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid/tests/data/000077500000000000000000000000001457263123000242475ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid/tests/data/bundle-01.yaml000066400000000000000000000037101457263123000266230ustar00rootroot00000000000000applications: candid: charm: {{ charm_path }} num_units: 1 options: location: canonical.candid.test identity-providers: |- - type: static name: static domain: test description: Default identity provider users: admin: name: User Admin email: admin password: admin_password groups: - group1 userOne: name: User One email: user.one@candid.test password: password1 groups: - groupOne userTwo: name: User Two email: user.two@candid.test password: password2 groups: - groupTwo private-key: 'zq+I3tQhxabqcDYRe2glUWnfynn5T0eDRPCv/EA+XoY=' public-key: 'bSwdHDgXZq/yH2NUp5PBGeGuXXnrixDUsUM3T46neWc=' admin-agent-public-key: 'bSwdHDgXZq/yH2NUp5PBGeGuXXnrixDUsUM3T46neWc=' resources: candid: {{ snap_path }} haproxy: charm: cs:haproxy channel: stable num_units: 1 expose: true series: bionic options: default_mode: tcp enable_monitoring: True services: |- - service_name: app-jimm service_host: "0.0.0.0" service_port: 443 service_options: - mode http - balance leastconn - cookie SRVNAME insert - option httpchk GET /debug/info HTTP/1.0 - acl metrics path -i /metrics - http-request deny if metrics server_options: check inter 2000 rise 2 fall 5 maxconn 4096 crts: [DEFAULT] - service_name: api_http service_host: "0.0.0.0" service_port: 80 service_options: - mode http - http-request redirect scheme https peering_mode: active-active postgresql: charm: ch:postgresql channel: stable num_units: 1 golang-github-canonical-candid-1.12.3/charms/candid/tests/integration/000077500000000000000000000000001457263123000256615ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid/tests/integration/__init__.py000066400000000000000000000001121457263123000277640ustar00rootroot00000000000000# Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. golang-github-canonical-candid-1.12.3/charms/candid/tests/integration/conftest.py000066400000000000000000000056221457263123000300650ustar00rootroot00000000000000import logging from pathlib import Path from typing import Tuple import pytest from integration.utils import build_snap_and_charm from pytest_operator.plugin import OpsTest LOGGER = logging.getLogger(__name__) # Fixtures to handle the deployment per each test suite. # ops_test is a module fixture, which kind of limits us in what we # can do regarding building artifacts required for the tests. # As such, we run a subproc validating the snap version # such that we don't have to try build it over and over. # # As for the charm, we should probably do the same. But for # now we use the built in ops_test.build_charm # TODO: Move this into setupTest funcs and turns the bundlepath & snap/charm build fixture # into session fixtures. Then pull bundle path into each setupTest lifecycle func and # deploy per each test suite. # TODO: Figure out why snap sometimes builds, sometimes doesn't ... @pytest.fixture(name="snap_and_charm_paths", scope="module") async def build_snap_and_charm_fixture(ops_test: OpsTest): LOGGER.info("Building snap and charm.") charm_directory = Path.cwd() root_directory = charm_directory.parent.parent.absolute() snap_path, charm_path = await build_snap_and_charm( root_directory, charm_directory, ops_test ) yield snap_path, charm_path @pytest.fixture( name="bundle_path", scope="module" ) # snap_and_charm_paths: Tuple[str, str]) def render_bundle_fixture(ops_test: OpsTest, snap_and_charm_paths: Tuple[str, str]): LOGGER.info("Rendering bundle with snap and charm paths.") charm_directory = Path.cwd() tests_directory = charm_directory.joinpath("tests") tests_data_directory = tests_directory.joinpath("data") bundle_path = tests_data_directory.joinpath("bundle-01.yaml") rendered_bundle_path = ops_test.render_bundle( bundle_path, charm_path=snap_and_charm_paths[1], snap_path=snap_and_charm_paths[0], ) LOGGER.info("Bundle path is: %s", str(rendered_bundle_path.absolute())) yield rendered_bundle_path # TODO: Move this into setupTest funcs and turns the bundlepath & snap/charm build fixture # into session fixtures. Then pull bundle path into each setupTest lifecycle func and # deploy per each test suite. @pytest.fixture(name="deploy_built_bundle", scope="module") async def deploy_bundle_function(ops_test: OpsTest, bundle_path: Path): juju_cmd = [ "deploy", "-m", ops_test.model_full_name, str(bundle_path.absolute()), ] rc, stdout, stderr = await ops_test.juju(*juju_cmd) if rc != 0: raise FailedToDeployBundleError(stderr, stdout) class FailedToDeployBundleError(Exception): """Exception raised when bundle fails to deploy. Attributes: stderr -- todo stdout -- todo """ def __init__(self, stderr, stdout): self.message = f"Bundle deploy failed: {(stderr or stdout).strip()}" super().__init__(self.message) golang-github-canonical-candid-1.12.3/charms/candid/tests/integration/test_relations.py000066400000000000000000000037271457263123000313030ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. import logging import integration.utils as utils import pytest from pytest_operator.plugin import OpsTest logger = logging.getLogger(__name__) APP_NAME = "candid" PG_NAME = "postgresql" HA_NAME = "haproxy" @pytest.mark.abort_on_fail @pytest.mark.usefixtures("deploy_built_bundle") class TestRelations: async def test_no_postgresql_relation(self, ops_test: OpsTest): async with ops_test.fast_forward(): await ops_test.model.wait_for_idle(apps=[APP_NAME], status="blocked") candid_unit = await utils.get_unit_by_name("candid", "0", ops_test.model.units) assert candid_unit.workload_status == "blocked" assert candid_unit.workload_status_message == "Waiting for postgres relation." async def test_add_postgresql_relation(self, ops_test: OpsTest): async with ops_test.fast_forward(): await ops_test.model.wait_for_idle(apps=[APP_NAME, PG_NAME]) await ops_test.model.add_relation(APP_NAME, "{}:db".format(PG_NAME)) async with ops_test.fast_forward(): await ops_test.model.wait_for_idle(apps=[APP_NAME], status="active") candid_unit = await utils.get_unit_by_name("candid", "0", ops_test.model.units) assert candid_unit.workload_status == "active" assert candid_unit.workload_status_message == "Ready" async def test_add_haproxy_relation(self, ops_test: OpsTest): async with ops_test.fast_forward(): await ops_test.model.wait_for_idle(apps=[APP_NAME, HA_NAME]) await ops_test.model.add_relation(APP_NAME, HA_NAME) async with ops_test.fast_forward(): await ops_test.model.wait_for_idle(apps=[APP_NAME], status="active") candid_unit = await utils.get_unit_by_name("candid", "0", ops_test.model.units) assert candid_unit.workload_status == "active" assert candid_unit.workload_status_message == "Ready" golang-github-canonical-candid-1.12.3/charms/candid/tests/integration/utils.py000066400000000000000000000111701457263123000273730ustar00rootroot00000000000000import logging import os from pathlib import Path from subprocess import PIPE, Popen, check_output from typing import Dict, Tuple from juju.application import Application from juju.unit import Unit from pytest_operator.plugin import OpsTest LOGGER = logging.getLogger(__name__) async def get_unit_by_name( unit_name: str, unit_index: str, unit_list: Dict[str, Unit] ) -> Unit: return unit_list.get( "{unitname}/{unitindex}".format(unitname=unit_name, unitindex=unit_index) ) async def get_application_by_name( app_name: str, app_list: Dict[str, Application] ) -> Application: return app_list.get(app_name) async def build_snap_and_charm( root_directory: str, charm_directory: str, ops_test: OpsTest ) -> Tuple[str, str]: """ Builds the snap and charm, returning a tuple of [snappath,charmpath] """ LOGGER.info("Building snap...") LOGGER.info("Root directory is {}".format(root_directory)) snap_exists, path = await check_if_snap_exists(root_directory, root_directory) if snap_exists: LOGGER.info("Snap already exists, skipping build.") snap_path = path else: snap_path = await build_snap_lxd(root_directory) LOGGER.info("Snap path is: %s", str(snap_path)) LOGGER.info("Building charm...") charm_path = await ops_test.build_charm(charm_directory) LOGGER.info("Charm path is: %s", str(charm_path.absolute())) return snap_path, charm_path async def build_snap_lxd(working_directory: Path) -> Path: snap_path = Path(str(working_directory.absolute())) """ Will build (& print sub proc output) a snap in LXD and return the Path to it """ # Instead of this (resources = {rsc.stem: rsc for rsc in resources}), # we run commands ourselves and move to tmp_path # additionally cleaning up ourselves. # resources = await ops_test.build_resources(build_snap_script, False) # # I wanted to see the build output for the snap and additionally the charm # in verbose mode too, hence, opted for this approach. p = Popen( # Having multipass issues, so lxd will have to do for now... # TODO: Figure out why multipass is trying to export /usr/sbin as a variable args="snapcraft --use-lxd", stdout=PIPE, stderr=PIPE, universal_newlines=True, shell=True, cwd=str(snap_path), bufsize=1, # We want immediate output for the snap building. ) # Create iter() with sentinel set to b'', such that # we can detect an effective EOF for the completed snapcraft process. for line in iter(p.stdout.readline, b""): LOGGER.info(line) # If the line contains "Snapped", we know the file name is # immediately after, as for whether this is reliable - probably not # and we could walk() the dir. But it works for now. if "Snapped" in line: snap_path = snap_path.joinpath(line.split(" ")[1]) # If it polls to none, we're certain it's finished. # We check both in case the process does output b'' and we accidentally # break the read too early. if line == "" and p.poll() is not None: break p.stdout.close() p.wait() if p.returncode != 0: raise FailedToBuildSnapError(p.returncode) return snap_path async def check_if_snap_exists(git_dir: Path, snap_build_dir: Path) -> Tuple[bool, str]: """ Finds if the snap exists already by version. If true, snap exists, else no no """ _command = [ "git", "describe", "--tags", # We use dirty such that we are aware we're making changes. # and that this build of the snap is not feasible. # Perhaps though, we should add a check to see if the working # tree is actually dirty and abort the entire test suite? # # Up for discussion. "--dirty", "--abbrev=0", ] git_version = check_output(_command, universal_newlines=True, cwd=git_dir) for fname in os.listdir(str(snap_build_dir)): if fname.endswith(".snap"): if git_version.strip() in fname.strip(): path = os.path.abspath( "{path}/{file}".format(path=str(snap_build_dir), file=fname.strip()) ) return True, path break return False, None class FailedToBuildSnapError(Exception): """Exception raised when snap fails to build. Attributes: respcode-- todo """ def __init__(self, respcode): self.message = "Snap failed to build with response code: {respcode} ".format( respcode=respcode ) super().__init__(self.message) golang-github-canonical-candid-1.12.3/charms/candid/tests/unit/000077500000000000000000000000001457263123000243155ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/candid/tests/unit/__init__.py000066400000000000000000000000741457263123000264270ustar00rootroot00000000000000import ops.testing ops.testing.SIMULATE_CAN_CONNECT = True golang-github-canonical-candid-1.12.3/charms/candid/tests/unit/test_charm.py000066400000000000000000000013131457263123000270160ustar00rootroot00000000000000# Copyright 2022 Ales Stimec # See LICENSE file for licensing details. # # Learn more about testing at: https://juju.is/docs/sdk/testing import unittest from ops.testing import Harness from charm import CandidCharm class TestCharm(unittest.TestCase): def setUp(self): self.harness = Harness(CandidCharm) self.addCleanup(self.harness.cleanup) self.harness.begin() def test_website_relation_joined(self): id = self.harness.add_relation("website", "apache2") self.harness.add_relation_unit(id, "apache2/0") data = self.harness.get_relation_data(id, self.harness.charm.unit.name) self.assertTrue(data) self.assertEqual(data["port"], "8081") golang-github-canonical-candid-1.12.3/charms/candid/tox.ini000066400000000000000000000041161457263123000235110ustar00rootroot00000000000000[tox] skipsdist=True envlist = lint unit integration skip_missing_interpreters = True [vars] application = candid src_path = {toxinidir}/src/ tst_path = {toxinidir}/tests/ lib_path = {toxinidir}/lib/charms/operator_libs_linux/v2 all_path = {[vars]src_path} {[vars]tst_path} [testenv] basepython = python3 setenv = PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path} PYTHONBREAKPOINT=ipdb.set_trace PY_COLORS=1 passenv = PYTHONPATH CHARM_BUILD_DIR MODEL_SETTINGS [testenv:fmt] description = Apply coding style standards to code deps = black isort commands = isort {[vars]all_path} {[vars]lib_path} black {[vars]all_path} {[vars]lib_path} [testenv:lint] description = Check code against coding style standards deps = black flake8 == 4.0.1 flake8-docstrings flake8-copyright flake8-builtins pylint pyproject-flake8 pep8-naming isort codespell yamllint -r{toxinidir}/requirements.txt commands = # uncomment the following 2 lines if this charm owns a lib codespell {[vars]lib_path} # pylint -E {[vars]lib_path} codespell {toxinidir}/. --skip {toxinidir}/.git --skip {toxinidir}/.tox \ --skip {toxinidir}/build --skip {toxinidir}/lib --skip {toxinidir}/venv \ --skip {toxinidir}/.mypy_cache --skip {toxinidir}/icon.svg pylint -E {[vars]src_path} # pflake8 wrapper supports config from pyproject.toml pflake8 --ignore=W503 {[vars]all_path} black --check --diff {[vars]all_path} {[vars]lib_path} --line-length 79 [testenv:integration] description = Run integration tests deps = -r {toxinidir}/requirements-dev.txt -r {toxinidir}/requirements-test.txt commands = pytest -ra {toxinidir}/tests/integration [testenv:unit] description = Run unit tests deps = pytest pytest-mock coverage[toml] -r{toxinidir}/requirements-dev.txt -r{toxinidir}/requirements-test.txt commands = coverage run --source={[vars]src_path},{[vars]lib_path} \ -m pytest --ignore={[vars]tst_path}integration -v --tb native -s {posargs} coverage report coverage xml golang-github-canonical-candid-1.12.3/charms/charm/000077500000000000000000000000001457263123000220445ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/charm/.gitignore000066400000000000000000000000161457263123000240310ustar00rootroot00000000000000/build/ *.pyc golang-github-canonical-candid-1.12.3/charms/charm/Makefile000066400000000000000000000005111457263123000235010ustar00rootroot00000000000000CHARM=/snap/bin/charm .PHONY: build build: CHARM_INTERFACES_DIR=interfaces $(CHARM) build --build-dir build layer-candid .PHONY: lint lint: @# ls -d hooks/* | grep -v -E '/(install|charmhelpers)$' flake8 \ layer-candid/reactive/candid.py \ layer-candid/lib/charms/layer/candid.py .PHONY: clean clean: -rm -rf build golang-github-canonical-candid-1.12.3/charms/charm/interfaces/000077500000000000000000000000001457263123000241675ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/charm/interfaces/candid/000077500000000000000000000000001457263123000254115ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/charm/interfaces/candid/interface.yaml000066400000000000000000000001541457263123000302350ustar00rootroot00000000000000name: candid summary: candid internal communication version: 1 repo: https://github.com/CanonicalLtd/candid golang-github-canonical-candid-1.12.3/charms/charm/interfaces/candid/peers.py000066400000000000000000000027061457263123000271060ustar00rootroot00000000000000#!/usr/bin/python # 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. from charms.reactive import Endpoint from charms.reactive import when, when_not, set_flag, clear_flag class CandidPeer(Endpoint): @when('endpoint.{endpoint_name}.joined') def changed(self): set_flag(self.expand_name('{endpoint_name}.connected')) set_flag(self.expand_name('{endpoint_name}.available')) @when_not('endpoint.{endpoint_name}.joined') def broken(self): clear_flag(self.expand_name('{endpoint_name}.available')) clear_flag(self.expand_name('{endpoint_name}.connected')) @property def addresses(self): """ A flat list of all private addresses received from related units. This list is de-duplicated and sorted by address, so it will be stable for change comparison. """ addrs = {u.received_raw['private-address'] for u in self.all_joined_units} return list(sorted(addrs)) golang-github-canonical-candid-1.12.3/charms/charm/layer-candid/000077500000000000000000000000001457263123000244005ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/charm/layer-candid/README000066400000000000000000000003151457263123000252570ustar00rootroot00000000000000# Candid Identity service The Candid server provides a macaroon-based authentication service. The charm is designed to by in a model using haproxy as a frontend and storing data in a postgresql database. golang-github-canonical-candid-1.12.3/charms/charm/layer-candid/config.yaml000066400000000000000000000066651457263123000265460ustar00rootroot00000000000000options: admin-agent-public-key: type: string default: "" description: | Base64 encoded 256-bit Ed25519 public key for admin agent. api-macaroon-timeout: type: string default: "48h" description: | The maximum age an API macaroon can get before requiring re-authorization. discharge-macaroon-timeout: type: string default: "48h" description: | The maximum age a discharge macaroon can get before it becomes invalid. discharge-token-timeout: type: string default: "48h" description: | The maximum age a discharge token can get before it becomes invalid. enable-email-login: type: boolean default: false description: | Enable the "login with email address" functionality on the authentication required page. http-proxy: type: string default: "" description: Address of proxy to use for outgoing HTTP connections. identity-providers: type: string default: "" description: | This is a Base64 encoded YAML array containing the identity provider definition. See https://github.com/CanonicalLtd/candid/blob/master/docs/configuration.md#identity-providers-1 for a full list of possible options. location: type: string default: "" description: | Publicly accessable URL of the identity manager (defaults to public address of unit). logging-config: type: string default: INFO description: Loggo logging configuration string. no-proxy: type: string default: "" description: | List of addresses that should not use the proxy specified in http-proxy. If specified this should be a comma-separated list of addresses. private-key: type: string default: "" description: Base64 encoded 256-bit Ed25519 private key of the server. public-key: type: string default: "" description: | Base64 encoded 256-bit Ed25519 public key, this should match the private key. redirect-login-trusted-urls: type: string default: "" description: | List of URLs that are trusted to return to when using the redirect login flow. redirect-login-trusted-domains: type: string default: "" description: | Comma separated list of domains for which all redirect paths are trusted. If a domain starts with "*." that is taken to be a wildcard and will match all subdomains of the specified domain. rendezvous-timeout: type: string default: "10m" description: | Amount of time that an interactive authentication request can be active before it is forgotten. The value must be a time duration specified as a decimal number followed by a unit from ns, us, ms, s, m, h for the time units between nanosecond and hour. skip-location-for-cookie-paths: type: boolean default: false description: | If true, it leaves cookies' Path value absolute, instead of seeting it relative to the path in the location config. mfa-rp-display-name: type: string default: "" description: | Display name for the multi-factor authentication relying party. mfa-rp-id: type: string default: "" description: | ID of the multi-factor authentication relying party (usually the FQDN for your site). mfa-rp-origin: type: string default: "" description: | Origin of the multi-factor authentication WebAuthn requests. golang-github-canonical-candid-1.12.3/charms/charm/layer-candid/copyright000066400000000000000000000000361457263123000263320ustar00rootroot00000000000000Copyright 2018 Canonical Ltd. golang-github-canonical-candid-1.12.3/charms/charm/layer-candid/icon.svg000066400000000000000000000051651457263123000260600ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/charm/layer-candid/layer.yaml000066400000000000000000000003351457263123000264010ustar00rootroot00000000000000repo: https://github.com/CanonicalLtd/candid includes: - interface:candid - interface:http - interface:pgsql - layer:basic - layer:leadership - layer:nagios - layer:snap options: snap: candid: channel: stable golang-github-canonical-candid-1.12.3/charms/charm/layer-candid/lib/000077500000000000000000000000001457263123000251465ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/charm/layer-candid/lib/charms/000077500000000000000000000000001457263123000264235ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/charm/layer-candid/lib/charms/layer/000077500000000000000000000000001457263123000275375ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/charm/layer-candid/lib/charms/layer/candid.py000066400000000000000000000030371457263123000313360ustar00rootroot00000000000000import base64 import binascii import json import subprocess import urllib.request import yaml class IdentityProvidersParseError(Exception): """Error parsing identity provider configuration.""" pass def generate_keypair(): """ Create a default keypair shared by all units in the application, if a keypair is not explicitely configured. """ res = subprocess.run( ("/snap/candid/current/bin/bakery-keygen", ), stdout=subprocess.PIPE) res.check_returncode() return json.loads(res.stdout.decode('utf-8')) def parse_identity_providers(idps): """ parse the identity-providers configuration option. """ b64err = None try: idps = base64.b64decode(idps, validate=True) except binascii.Error as e: # Be tolerant of non-base64 values, to facilitate upgrades from # earlier charm versions. b64err = e try: return yaml.safe_load(idps) except yaml.YAMLError as e: msg = "error parsing identity-providers: {}".format(e) if b64err: msg += ", {}".format(b64err) raise IdentityProvidersParseError() def update_config(file, config): with open(file) as f: appconfig = yaml.safe_load(f) for k, v in config.items(): appconfig[k] = v with open(file, 'wb') as f: f.write(yaml.dump(appconfig, encoding="utf-8")) def get_version(): with urllib.request.urlopen('http://localhost:8081/debug/info') as resp: data = json.loads(resp.read().decode('utf-8')) return data.get('Version', '') golang-github-canonical-candid-1.12.3/charms/charm/layer-candid/metadata.yaml000066400000000000000000000010111457263123000270350ustar00rootroot00000000000000name: candid display-name: candid summary: Candid identity server. maintainer: JAAS Developers description: | Candid macaroon-based authentication service. tags: - web_server - authenticator - authentication - identity series: - focal - bionic - xenial peers: candid: interface: candid provides: website: interface: http requires: postgres: interface: pgsql resources: candid: type: file filename: candid.snap description: "Candid snap file" golang-github-canonical-candid-1.12.3/charms/charm/layer-candid/reactive/000077500000000000000000000000001457263123000262025ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/charms/charm/layer-candid/reactive/candid.py000066400000000000000000000122271457263123000300020ustar00rootroot00000000000000from charmhelpers.contrib.charmsupport import nrpe from charmhelpers.core import hookenv, host from charms import leadership from charms.layer import candid from charms.reactive import ( any_file_changed, clear_flag, endpoint_from_flag, hook, register_trigger, set_flag, when, when_not, ) CONFIG_FILE = '/var/snap/candid/current/config.yaml' register_trigger(when='candid.available', clear_flag='candid.configured') register_trigger(when='config.changed', clear_flag='candid.configured') register_trigger(when='leadership.set.private-key', clear_flag='candid.configured') register_trigger(when='leadership.set.public-key', clear_flag='candid.configured') register_trigger(when='postgres.master.changed', clear_flag='candid.configured') @when('snap.installed.candid') @when('leadership.is_leader') @when_not('leadership.set.private-key') def create_keypair(): hookenv.status_set('maintenance', 'Generating default keypair') key = candid.generate_keypair() leadership.leader_set({"private-key": key["private"]}) leadership.leader_set({"public-key": key["public"]}) @when_not('candid.port_opened') def open_port(): hookenv.status_set('maintenance', 'Opening port') hookenv.open_port(8081) set_flag('candid.port_opened') @when_not('candid.configured') def write_config_file(): cc = hookenv.config() lc = leadership.leader_get() config = { "api-macaroon-timeout": cc["api-macaroon-timeout"], "discharge-macaroon-timeout": cc["discharge-macaroon-timeout"], "discharge-token-timeout": cc["discharge-token-timeout"], "enable-email-login": cc["enable-email-login"], "logging-config": cc["logging-config"], "private-addr": hookenv.unit_private_ip(), "rendezvous-timeout": cc["rendezvous-timeout"], "skip-location-for-cookie-paths": cc["skip-location-for-cookie-paths"], } if cc["admin-agent-public-key"]: config["admin-agent-public-key"] = cc["admin-agent-public-key"] if cc["http-proxy"]: config["http-proxy"] = cc["http-proxy"] # extend no-proxy to include all candid units. no_proxy = [cc["no-proxy"]] if not no_proxy[0]: no_proxy = no_proxy[1:] ep = endpoint_from_flag('candid.connected') if ep: no_proxy.extend(ep.addresses) config["no-proxy"] = ",".join(no_proxy) if cc["identity-providers"]: try: config["identity-providers"] = \ candid.parse_identity_providers(cc["identity-providers"]) except candid.IdentityProvidersParseError as e: hookenv.log("invalid identity providers: {}".format(e), level="error") if cc["location"]: config["location"] = cc["location"] if cc["private-key"]: config["private-key"] = cc["private-key"] elif lc.get("private-key"): config["private-key"] = lc["private-key"] if cc["public-key"]: config["public-key"] = cc["public-key"] elif lc.get("public-key"): config["public-key"] = lc["public-key"] if cc["redirect-login-trusted-urls"]: config["redirect-login-trusted-urls"] = \ _parse_list(cc["redirect-login-trusted-urls"]) if cc["redirect-login-trusted-domains"]: config["redirect-login-trusted-domains"] = \ _parse_list(cc["redirect-login-trusted-domains"]) if cc["mfa-rp-id"]: config["mfa-rp-id"] = cc["mfa-rp-id"] if cc["mfa-rp-display-name"]: config["mfa-rp-display-name"] = cc["mfa-rp-display-name"] if cc["mfa-rp-origin"]: config["mfa-rp-origin"] = cc["mfa-rp-origin"] pg = endpoint_from_flag('postgres.master.available') if pg: config["storage"] = { "type": "postgres", "connection-string": str(pg.master), } else: config["storage"] = {"type": "memory"} candid.update_config(CONFIG_FILE, config) set_flag('candid.configured') set_flag('candid.restart') @when('candid.restart') def restart_candid(): clear_flag('candid.restart') if not any_file_changed([CONFIG_FILE]): hookenv.log("not restarting: config file unchanged", level="info") return hookenv.status_set('maintenance', 'Restarting candid') host.service_restart('snap.candid.candidsrv.service') update_status() @hook('update-status') def update_status(): try: hookenv.application_version_set(candid.get_version()) hookenv.status_set('active', '') except Exception as e: hookenv.log("cannot get version: {}".format(e), level="warning") @when('nrpe-external-master.available') def configure_nrpe(): nrpeconfig = nrpe.NRPE() nrpeconfig.add_check( shortname="candid", description='Check candid running', check_cmd='check_http -w 2 -c 10 -I {} -p 8081 -u /debug/info'.format( hookenv.unit_private_ip(), ) ) nrpeconfig.write() @when('website.available') def website_available(): ep = endpoint_from_flag('website.available') ep.configure(8081) def _parse_list(s): if not s: return None return [t.strip() for t in s.split(",")] golang-github-canonical-candid-1.12.3/cmd/000077500000000000000000000000001457263123000202405ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/cmd/candid/000077500000000000000000000000001457263123000214625ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/cmd/candid/internal/000077500000000000000000000000001457263123000232765ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/000077500000000000000000000000001457263123000250525ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/acl.go000066400000000000000000000073641457263123000261520ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd import ( "context" "github.com/juju/aclstore/v2/aclclient" "github.com/juju/cmd/v3" "github.com/juju/gnuflag" errgo "gopkg.in/errgo.v1" ) var aclCmdDoc = ` The acl command is used to manage ACLs. ` func newACLCommand(cc *candidCommand) cmd.Command { supercmd := cmd.NewSuperCommand(cmd.SuperCommandParams{ Name: "acl", Doc: aclCmdDoc, Purpose: "manage candid ACLs", }) supercmd.Register(&aclGrantCommand{candidCommand: cc}) supercmd.Register(&aclRevokeCommand{candidCommand: cc}) supercmd.Register(&aclShowCommand{candidCommand: cc}) return supercmd } var aclShowDoc = ` The show command shows the members of the specified ACL. candid acl show read-user ` type aclShowCommand struct { *candidCommand name string out cmd.Output } func (c *aclShowCommand) Info() *cmd.Info { return &cmd.Info{ Name: "show", Purpose: "show acl members", Doc: aclShowDoc, } } func (c *aclShowCommand) SetFlags(f *gnuflag.FlagSet) { c.candidCommand.SetFlags(f) c.out.AddFlags(f, "smart", cmd.DefaultFormatters.Formatters()) } func (c *aclShowCommand) Init(args []string) error { if err := c.candidCommand.Init(nil); err != nil { return errgo.Mask(err) } if len(args) < 1 { return errgo.New("ACL name required") } if len(args) > 1 { return errgo.New("only one ACL may be specified") } c.name = args[0] return nil } func (c *aclShowCommand) Run(ctxt *cmd.Context) error { defer c.Close(ctxt) client, err := aclClient(ctxt, c.candidCommand) if err != nil { return errgo.Mask(err) } ctx := context.Background() acl, err := client.Get(ctx, c.name) if err != nil { return errgo.Mask(err) } return errgo.Mask(c.out.Write(ctxt, acl)) } var aclGrantDoc = ` The grant command adds users to the specified ACL. candid acl grant read-user alice bob ` type aclGrantCommand struct { *candidCommand name string users []string } func (c *aclGrantCommand) Info() *cmd.Info { return &cmd.Info{ Name: "grant", Purpose: "add users to an ACL", Doc: aclGrantDoc, } } func (c *aclGrantCommand) Init(args []string) error { if err := c.candidCommand.Init(nil); err != nil { return errgo.Mask(err) } if len(args) < 2 { return errgo.New("ACL name and at least one user required") } c.name = args[0] c.users = args[1:] return nil } func (c *aclGrantCommand) Run(ctxt *cmd.Context) error { defer c.Close(ctxt) client, err := aclClient(ctxt, c.candidCommand) if err != nil { return errgo.Mask(err) } return errgo.Mask(client.Add(context.Background(), c.name, c.users)) } var aclRevokeDoc = ` The revoke command removes users from the specified ACL. candid acl revoke read-user alice bob ` type aclRevokeCommand struct { *candidCommand name string users []string } func (c *aclRevokeCommand) Info() *cmd.Info { return &cmd.Info{ Name: "revoke", Purpose: "remove users from an ACL", Doc: aclRevokeDoc, } } func (c *aclRevokeCommand) Init(args []string) error { if err := c.candidCommand.Init(nil); err != nil { return errgo.Mask(err) } if len(args) < 2 { return errgo.New("ACL name and at least one user required") } c.name = args[0] c.users = args[1:] return nil } func (c *aclRevokeCommand) Run(ctxt *cmd.Context) error { defer c.Close(ctxt) client, err := aclClient(ctxt, c.candidCommand) if err != nil { return errgo.Mask(err) } return errgo.Mask(client.Remove(context.Background(), c.name, c.users)) } func aclClient(ctxt *cmd.Context, c *candidCommand) (*aclclient.Client, error) { bClient, err := c.BakeryClient(ctxt) if err != nil { return nil, errgo.Mask(err) } return aclclient.New(aclclient.NewParams{ BaseURL: candidURL(c.url) + "/acl", Doer: bClient, }), nil } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/acl_test.go000066400000000000000000000051771457263123000272110ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd_test import ( "context" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" ) type aclSuite struct { fixture *fixture } func TestACL(t *testing.T) { qtsuite.Run(qt.New(t), &aclSuite{}) } func (s *aclSuite) Init(c *qt.C) { s.fixture = newFixture(c) } func (s *aclSuite) TestACLShow(c *qt.C) { err := s.fixture.aclStore.Set(context.Background(), "read-user", []string{"admin@candid", "alice", "bob"}) c.Assert(err, qt.IsNil) stdout := s.fixture.CheckSuccess(c, "-a", "admin.agent", "acl", "show", "read-user") c.Assert(stdout, qt.Equals, ` admin@candid alice bob `[1:]) } func (s *aclSuite) TestACLShowNoACL(c *qt.C) { s.fixture.CheckError(c, 2, `ACL name required`, "-a", "admin.agent", "acl", "show") } func (s *aclSuite) TestACLShowTwoACLs(c *qt.C) { s.fixture.CheckError(c, 2, `only one ACL may be specified`, "-a", "admin.agent", "acl", "show", "read-user", "write-user") } func (s *aclSuite) TestACLShowInvalid(c *qt.C) { s.fixture.CheckError(c, 1, `Get http://.*/acl/no-such-acl: ACL not found`, "-a", "admin.agent", "acl", "show", "no-such-acl") } func (s *aclSuite) TestACLGrant(c *qt.C) { s.fixture.CheckNoOutput(c, "-a", "admin.agent", "acl", "grant", "read-user", "alice", "bob") acl, err := s.fixture.aclStore.Get(context.Background(), "read-user") c.Assert(err, qt.IsNil) c.Assert(acl, qt.DeepEquals, []string{"admin@candid", "alice", "bob", "userinfo@candid"}) } func (s *aclSuite) TestACLGrantNoArguments(c *qt.C) { s.fixture.CheckError(c, 2, `ACL name and at least one user required`, "-a", "admin.agent", "acl", "grant") } func (s *aclSuite) TestACLGrantInvalid(c *qt.C) { s.fixture.CheckError(c, 1, `Post http://.*/acl/no-such-acl: ACL not found`, "-a", "admin.agent", "acl", "grant", "no-such-acl", "bob") } func (s *aclSuite) TestACLRevoke(c *qt.C) { err := s.fixture.aclStore.Set(context.Background(), "read-user", []string{"admin@candid", "alice", "bob"}) c.Assert(err, qt.IsNil) s.fixture.CheckNoOutput(c, "-a", "admin.agent", "acl", "revoke", "read-user", "bob") acl, err := s.fixture.aclStore.Get(context.Background(), "read-user") c.Assert(err, qt.IsNil) c.Assert(acl, qt.DeepEquals, []string{"admin@candid", "alice"}) } func (s *aclSuite) TestACLRevokeNoArguments(c *qt.C) { s.fixture.CheckError(c, 2, `ACL name and at least one user required`, "-a", "admin.agent", "acl", "revoke") } func (s *aclSuite) TestACLRevokeInvalid(c *qt.C) { s.fixture.CheckError(c, 1, `Post http://.*/acl/no-such-acl: ACL not found`, "-a", "admin.agent", "acl", "revoke", "no-such-acl", "bob") } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/add-group.go000066400000000000000000000027221457263123000272660ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd import ( "context" "github.com/juju/cmd/v3" "gopkg.in/errgo.v1" "github.com/canonical/candid/params" ) type addGroupCommand struct { userCommand groups []string } func newAddGroupCommand(cc *candidCommand) cmd.Command { c := &addGroupCommand{} c.candidCommand = cc return c } var addGroupDoc = ` The add-group command adds the specified user to the specified group, or groups. To add the group-1 and group-2 groups to the user bob: candid add-group -u bob group-1 group-2 To add the group-1 and group-2 groups to the user with the email address bob@example.com: candid add-group -e bob@example.com group-1 group-2 ` func (c *addGroupCommand) Info() *cmd.Info { return &cmd.Info{ Name: "add-group", Args: "[group...]", Purpose: "add a user to groups", Doc: addGroupDoc, } } func (c *addGroupCommand) Init(args []string) error { c.groups = args return errgo.Mask(c.userCommand.Init(nil)) } func (c *addGroupCommand) Run(ctxt *cmd.Context) error { defer c.Close(ctxt) ctx := context.Background() username, err := c.lookupUser(ctxt) if err != nil { return errgo.Mask(err) } client, err := c.Client(ctxt) if err != nil { return errgo.Mask(err) } err = client.ModifyUserGroups(ctx, ¶ms.ModifyUserGroupsRequest{ Username: username, Groups: params.ModifyGroups{ Add: c.groups, }, }) return errgo.Mask(err) } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/add-group_test.go000066400000000000000000000057201457263123000303260ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd_test import ( "context" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/canonical/candid/candidtest" "github.com/canonical/candid/store" ) type addGroupSuite struct { fixture *fixture } func TestAddGroup(t *testing.T) { qtsuite.Run(qt.New(t), &addGroupSuite{}) } func (s *addGroupSuite) Init(c *qt.C) { s.fixture = newFixture(c) } func (s *addGroupSuite) TestAddGroup(c *qt.C) { ctx := context.Background() candidtest.AddIdentity(ctx, s.fixture.store, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", }) s.fixture.CheckNoOutput(c, "add-group", "-a", "admin.agent", "-u", "bob", "test1", "test2") identity := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), } err := s.fixture.store.Identity(ctx, &identity) c.Assert(err, qt.IsNil) c.Assert(identity.Groups, qt.DeepEquals, []string{"test1", "test2"}) } func (s *addGroupSuite) TestAddGroupForEmail(c *qt.C) { ctx := context.Background() candidtest.AddIdentity(ctx, s.fixture.store, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", Email: "bob@example.com", }) s.fixture.CheckNoOutput(c, "add-group", "-a", "admin.agent", "-e", "bob@example.com", "test1", "test2") identity := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), } err := s.fixture.store.Identity(ctx, &identity) c.Assert(err, qt.IsNil) c.Assert(identity.Groups, qt.DeepEquals, []string{"test1", "test2"}) } func (s *addGroupSuite) TestAddGroupForEmailNotFound(c *qt.C) { s.fixture.CheckError( c, 1, `no user found for email "alice@example.com"`, "add-group", "-a", "admin.agent", "-e", "alice@example.com", "test1", "test2", ) } func (s *addGroupSuite) TestAddGroupForEmailMultipleUsers(c *qt.C) { ctx := context.Background() identities := []store.Identity{{ ProviderID: store.MakeProviderIdentity("test", "alice"), Username: "alice", Email: "bob@example.com", }, { ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", Email: "bob@example.com", }} for _, id := range identities { candidtest.AddIdentity(ctx, s.fixture.store, &id) } s.fixture.CheckError( c, 1, `more than one user found with email "bob@example.com" \(alice, bob\)`, "add-group", "-a", "admin.agent", "-e", "bob@example.com", "test1", "test2", ) } func (s *addGroupSuite) TestAddGroupNoUser(c *qt.C) { s.fixture.CheckError( c, 2, `no user specified, please specify either username or email`, "add-group", "-a", "admin.agent", "test1", "test2", ) } func (s *addGroupSuite) TestAddGroupUserAndEmail(c *qt.C) { s.fixture.CheckError( c, 2, `both username and email specified, please specify either username or email`, "add-group", "-a", "admin.agent", "-u", "bob", "-e", "bob@example.com", "test1", "test2", ) } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/agent_test.go000066400000000000000000000115421457263123000275410ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd_test import ( "context" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakerytest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" errgo "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" macaroon "gopkg.in/macaroon.v2" "github.com/canonical/candid/candidclient" ) var agentBakeryKey bakery.KeyPair func init() { if err := agentBakeryKey.Public.UnmarshalText([]byte("VM/0uXz4QvXJ7AB0F2RJaIuPqpgoQNYySeNEjUePyls=")); err != nil { panic(err) } if err := agentBakeryKey.Private.UnmarshalText([]byte("xctCL3iB2Qa9fvjGOPKU/3GHYMiqd4KJSF4Z44SGyRo=")); err != nil { panic(err) } } // AgentDischarger is a bakerytest.Discharger that implements // visit by providing the agent login flow. type AgentDischarger struct { *bakerytest.Discharger bakery *identchecker.Bakery agents map[string]*bakery.PublicKey } // NewAgentDischarger creates an AgentDischarger. func NewAgentDischarger() *AgentDischarger { d := &AgentDischarger{ Discharger: bakerytest.NewDischarger(nil), bakery: identchecker.NewBakery(identchecker.BakeryParams{ Key: &agentBakeryKey, IdentityClient: agentLoginIdentityClient{}, Authorizer: identchecker.OpenAuthorizer, }), agents: make(map[string]*bakery.PublicKey), } srv := &httprequest.Server{ ErrorMapper: httpbakery.ErrorToResponse, } d.Discharger.CheckerP = d d.Discharger.AddHTTPHandlers([]httprequest.Handler{srv.Handle(d.visit)}) return d } // agentMacaroonRequest represents a request to get the // agent macaroon that, when discharged, becomes // the discharge token to complete the discharge. type agentMacaroonRequest struct { httprequest.Route `httprequest:"GET /login/agent"` Username string `httprequest:"username,form"` PublicKey *bakery.PublicKey `httprequest:"public-key,form"` } type agentMacaroonResponse struct { Macaroon *bakery.Macaroon `json:"macaroon"` } // visit implements http.Handler. It performs the agent login interaction flow. func (d *AgentDischarger) visit(p httprequest.Params, req *agentMacaroonRequest) (*agentMacaroonResponse, error) { m, err := d.bakery.Oven.NewMacaroon( p.Context, httpbakery.RequestVersion(p.Request), []checkers.Caveat{ candidclient.UserDeclaration(req.Username), bakery.LocalThirdPartyCaveat(req.PublicKey, httpbakery.RequestVersion(p.Request)), }, identchecker.LoginOp, ) if err != nil { return nil, errgo.Mask(err) } return &agentMacaroonResponse{ Macaroon: m, }, nil } func (d *AgentDischarger) CheckThirdPartyCaveat(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { if p.Token == nil || p.Token.Kind != "agent" { ierr := httpbakery.NewInteractionRequiredError(nil, p.Request) agent.SetInteraction(ierr, "/login/agent") return nil, ierr } var ms macaroon.Slice if err := ms.UnmarshalBinary(p.Token.Value); err != nil { return nil, errgo.Mask(err) } ai, err := d.bakery.Checker.Auth(ms).Allow(ctx, identchecker.LoginOp) if err != nil { return nil, errgo.Mask(err) } return []checkers.Caveat{ candidclient.UserDeclaration(ai.Identity.Id()), }, nil } type agentLoginIdentityClient struct{} func (c agentLoginIdentityClient) IdentityFromContext(ctx context.Context) (identchecker.Identity, []checkers.Caveat, error) { return nil, nil, nil } func (c agentLoginIdentityClient) DeclaredIdentity(ctx context.Context, declared map[string]string) (identchecker.Identity, error) { username, ok := declared["username"] if !ok { return nil, errgo.Newf("no declared user") } return identchecker.SimpleIdentity(username), nil } // IdentityClient creates an identity client that will authenticate with // an AgentLogin being served by a InteractiveDischarger at the given // location. func IdentityClient(location string) identchecker.IdentityClient { return &identityClient{ location: location, } } type identityClient struct { location string } // IdentityFromContext implements identchecker.IdentityClient.IdentityFromContext. func (c identityClient) IdentityFromContext(ctx context.Context) (identchecker.Identity, []checkers.Caveat, error) { return nil, []checkers.Caveat{{ Location: c.location, Condition: "is-authenticated-user", }}, nil } // DeclaredIdentity implements identchecker.IdentityClient.DeclaredIdentity. func (c identityClient) DeclaredIdentity(ctx context.Context, declared map[string]string) (identchecker.Identity, error) { username, ok := declared["username"] if !ok { return nil, errgo.Newf("no declared user") } return identchecker.SimpleIdentity(username), nil } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/clear-mfa-credentials.go000066400000000000000000000026701457263123000315300ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd import ( "context" "github.com/juju/cmd/v3" "gopkg.in/errgo.v1" "github.com/canonical/candid/params" ) type clearMFACredentialsCommand struct { userCommand } func newClearMFACredentialsCommand(cc *candidCommand) cmd.Command { c := &clearMFACredentialsCommand{} c.candidCommand = cc return c } var clearMFACredentialsCommandDoc = ` The clear-mfa-credentials command removes all multi-factor authentication credentials for the specified user. candid clear-mfa-credentials bob ` func (c *clearMFACredentialsCommand) Info() *cmd.Info { return &cmd.Info{ Name: "clear-mfa-credentials", Args: "username", Purpose: "clear MFA credentials", Doc: clearMFACredentialsCommandDoc, } } func (c *clearMFACredentialsCommand) Init(args []string) error { if len(args) != 1 { return errgo.Newf("expected 1 argument, got %d", len(args)) } c.username = args[0] return errgo.Mask(c.userCommand.Init(nil)) } func (c *clearMFACredentialsCommand) Run(ctxt *cmd.Context) error { defer c.Close(ctxt) ctx := context.Background() username, err := c.lookupUser(ctxt) if err != nil { return errgo.Mask(err) } client, err := c.Client(ctxt) if err != nil { return errgo.Mask(err) } err = client.ClearUserMFACredentials(ctx, ¶ms.ClearUserMFACredentialsRequest{ Username: username, }, ) return errgo.Mask(err) } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/clear-mfa-credentials_test.go000066400000000000000000000024271457263123000325670ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd_test import ( "context" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/canonical/candid/candidtest" "github.com/canonical/candid/store" ) type clearMFACredentialsSuite struct { fixture *fixture } func TestClearMFACredentials(t *testing.T) { qtsuite.Run(qt.New(t), &clearMFACredentialsSuite{}) } func (s *clearMFACredentialsSuite) Init(c *qt.C) { s.fixture = newFixture(c) } func (s *clearMFACredentialsSuite) TestClearMFACredentials(c *qt.C) { ctx := context.Background() candidtest.AddIdentity(ctx, s.fixture.store, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", }) err := s.fixture.store.AddMFACredential(context.Background(), store.MFACredential{ ID: []byte("test-id-1"), Name: "test name", ProviderID: store.MakeProviderIdentity("test", "bob"), }) c.Assert(err, qt.Equals, nil) s.fixture.CheckNoOutput(c, "clear-mfa-credentials", "-a", "admin.agent", "bob") creds, err := s.fixture.store.UserMFACredentials(context.Background(), string(store.MakeProviderIdentity("test", "bob"))) c.Assert(err, qt.Equals, nil) c.Assert(creds, qt.HasLen, 0) } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/command.go000066400000000000000000000252311457263123000270220ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd import ( "context" "crypto/tls" "crypto/x509" "encoding/json" "fmt" "io/ioutil" "net/http" "os" "path/filepath" "strings" "sync" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" "github.com/juju/cmd/v3" "github.com/juju/gnuflag" cookiejar "github.com/juju/persistent-cookiejar" "golang.org/x/net/publicsuffix" "gopkg.in/errgo.v1" "github.com/canonical/candid/candidclient" "github.com/canonical/candid/params" "github.com/canonical/candid/version" ) // jujuLoggingConfigEnvKey matches osenv.JujuLoggingConfigEnvKey // in the Juju project. const jujuLoggingConfigEnvKey = "JUJU_LOGGING_CONFIG" var cmdDoc = ` Manage the users on an identity server. By default the identity server at https://api.jujucharms.com/identity will be modified. This can be overridden either by setting the CANDID_URL environment variable, or by setting the --candid-url command line parameter. To use agent credentials for Candid operations, use the --agent flag or specify the BAKERY_AGENT_FILE environment variable, both of which hold the path to a file containing agent credentials in JSON format (see the create-agent subcommand for details). To configure additional CA certificates for the client an environment variable CANDID_CA_CERTS can be used. This contains a colon separated list of files which should each contain a list of PEM encoded certificates. All of these certificates will be added to the certificates from the system pool. Any file in the list which cannot be found, or cannot be read by the current user will be silently skipped. ` func New() cmd.Command { c := new(candidCommand) supercmd := cmd.NewSuperCommand(cmd.SuperCommandParams{ Name: "candid", Doc: cmdDoc, Purpose: "manage users on an identity server", Log: &cmd.Log{ DefaultConfig: os.Getenv(jujuLoggingConfigEnvKey), }, GlobalFlags: c, Version: version.VersionInfo.Version, }) supercmd.Register(newACLCommand(c)) supercmd.Register(newAddGroupCommand(c)) supercmd.Register(newCreateAgentCommand(c)) supercmd.Register(newFindCommand(c)) supercmd.Register(newRemoveGroupCommand(c)) supercmd.Register(newShowCommand(c)) supercmd.Register(newClearMFACredentialsCommand(c)) return supercmd } // candidCommand is a cmd.Command that provides a client for communicating // with an identity manager. The identity manager can be sepcified via // the command line, or using the CANDID_URL environment variable. type candidCommand struct { cmd.CommandBase url string agentFile string // mu protects the fields below it. mu sync.Mutex bakeryClient *httpbakery.Client client *candidclient.Client jar *cookiejar.Jar } // Close must be called at the end of a command's Run to ensure that // cookies are saved. func (c *candidCommand) Close(ctxt *cmd.Context) { c.mu.Lock() defer c.mu.Unlock() if c.jar == nil { return } if err := c.jar.Save(); err != nil { fmt.Fprintf(ctxt.Stderr, "cannot save cookies: %v", err) } c.jar = nil } // AddFlags implements cmd.FlagAdder to add global flags // to the flag set. func (c *candidCommand) AddFlags(f *gnuflag.FlagSet) { f.StringVar(&c.url, "candid-url", "", "URL of the identity server (defaults to $CANDID_URL)") f.StringVar(&c.agentFile, "a", "", "name of file containing agent login details") f.StringVar(&c.agentFile, "agent", "", "") } // BakeryClient creates a new httpbakery.Client using the parameters specified // in the flags and environment. func (c *candidCommand) BakeryClient(ctxt *cmd.Context) (*httpbakery.Client, error) { c.mu.Lock() defer c.mu.Unlock() return c.bakeryClientLocked(ctxt) } // bakeryClientLocked creates a new httpbakery.Client using the parameters specified // in the flags and environment. bakeryClient must be called with the mutex locked. func (c *candidCommand) bakeryClientLocked(ctxt *cmd.Context) (*httpbakery.Client, error) { if c.bakeryClient != nil { return c.bakeryClient, nil } bClient := httpbakery.NewClient() var authInfo *agent.AuthInfo if c.agentFile != "" { ai, err := readAgentFile(ctxt.AbsPath(c.agentFile)) if err != nil { return nil, errgo.Notef(err, "cannot load agent information") } authInfo = ai } else if ai, err := agent.AuthInfoFromEnvironment(); err == nil { authInfo = ai } else if errgo.Cause(err) != agent.ErrNoAuthInfo { return nil, errgo.Mask(err) } if authInfo != nil { // Agent authentication has been specified, so we probably don't // want to use existing cookies (which might be logged in as a different // user) or to fall back to interactive authentication. agent.SetUpAuth(bClient, authInfo) } else { jar, err := cookiejar.New(&cookiejar.Options{ PublicSuffixList: publicsuffix.List, }) if err != nil { return nil, errgo.Mask(err) } c.jar = jar bClient.Client.Jar = jar bClient.AddInteractor(httpbakery.WebBrowserInteractor{}) } if err := c.loadCACerts(bClient.Client); err != nil { return nil, errgo.Mask(err) } c.bakeryClient = bClient return bClient, nil } // loadCACerts loads any certificates found in the files specified by // CANDID_CA_CERTS, if any, and adds them to the system CA certificates // for this client. func (c *candidCommand) loadCACerts(client *http.Client) error { certPool, err := x509.SystemCertPool() if err != nil { return errgo.Notef(err, "cannot load system CA certificates") } for _, fn := range filepath.SplitList(os.Getenv("CANDID_CA_CERTS")) { buf, err := ioutil.ReadFile(fn) if os.IsNotExist(err) || os.IsPermission(err) { // If the file doesn't exist, or is not readable // ignore it. This allows the environment // variable to be set with potential paths even // when there are no certificates to load. continue } if err != nil { return errgo.Notef(err, "cannot load CA certificates") } certPool.AppendCertsFromPEM(buf) } client.Transport = &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: certPool, }, } return nil } // Client creates a new candidclient.Client using the parameters specified // in the flags and environment. func (c *candidCommand) Client(ctxt *cmd.Context) (*candidclient.Client, error) { c.mu.Lock() defer c.mu.Unlock() if c.client != nil { return c.client, nil } bClient, err := c.bakeryClientLocked(ctxt) if err != nil { return nil, errgo.Mask(err) } candidURL := candidURL(c.url) client, err := candidclient.New(candidclient.NewParams{ BaseURL: candidURL, Client: bClient, }) if err != nil { return nil, errgo.Mask(err) } c.client = client return client, nil } func candidURL(url string) string { if url != "" { return url } if url := os.Getenv("CANDID_URL"); url != "" { return url } return candidclient.Production } // usercmd is a cmd.Command that provides the ability to lookup and // manipulate a user that is specified on the command line either by // username or email address. Commands which wish to perform operations // on a particular user should embed this type and use lookupUser to find // the username to use in the subsequent requests. type userCommand struct { *candidCommand username string email string } func (c *userCommand) SetFlags(f *gnuflag.FlagSet) { c.candidCommand.SetFlags(f) f.StringVar(&c.username, "u", "", "username of the user") f.StringVar(&c.username, "username", "", "") f.StringVar(&c.email, "e", "", "email address of the user") f.StringVar(&c.email, "email", "", "") } func (c *userCommand) Init(args []string) error { if c.username == "" && c.email == "" { return errgo.New("no user specified, please specify either username or email") } else if c.username != "" && c.email != "" { return errgo.New("both username and email specified, please specify either username or email") } return errgo.Mask(c.candidCommand.Init(args)) } // AllowInterspersedFlags implements cmd.Command.AllowInterspersedFlags, // by making them not allowed. func (c *userCommand) AllowInterspersedFlags() bool { return false } // lookupUser returns the username specified by the command line flags. func (c *userCommand) lookupUser(ctxt *cmd.Context) (params.Username, error) { if c.username != "" { return params.Username(c.username), nil } client, err := c.Client(ctxt) if err != nil { return "", errgo.Mask(err) } users, err := client.QueryUsers(context.Background(), ¶ms.QueryUsersRequest{ Email: c.email, }) if err != nil { return "", errgo.Mask(err) } switch len(users) { case 0: return "", errgo.Newf("no user found for email %q", c.email) case 1: return params.Username(users[0]), nil } // Note: it is expected that for the most part this situation // should not come up as an identity server will not have many // identity providers and it is expected that they will not allow // more than one user to be registered with a unique email // address. There are however some situations in which this will // be possible. One case is when the user is a jujucharms.com // user and a snappy user which the identity server will keep // separate for implementation reasons, but could represent the // same Ubuntu SSO user. return "", errgo.Newf("more than one user found with email %q (%s)", c.email, strings.Join(users, ", ")) } func publicKeyVar(f *gnuflag.FlagSet, key **bakery.PublicKey, name string, usage string) { f.Var(publicKeyValue{key}, name, usage) } type publicKeyValue struct { key **bakery.PublicKey } // Set implements gnuflag.Getter.Set. func (v publicKeyValue) Set(s string) error { var k bakery.PublicKey if err := k.UnmarshalText([]byte(s)); err != nil { return errgo.Mask(err) } *v.key = &k return nil } // String implements gnuflag.Getter.String. func (v publicKeyValue) String() string { if *v.key == nil { return `""` } // Marshaling a key can never fail (and even // if it could, there's no way of returning an error here) data, _ := (*v.key).MarshalText() return fmt.Sprintf("%q", data) } // Get implements gnuflag.Getter.Get. func (v publicKeyValue) Get() interface{} { return *v.key } func readAgentFile(f string) (*agent.AuthInfo, error) { data, err := ioutil.ReadFile(f) if err != nil { return nil, errgo.Mask(err, os.IsNotExist) } var v agent.AuthInfo if err := json.Unmarshal(data, &v); err != nil { return nil, errgo.Notef(err, "cannot parse agent data from %q", f) } return &v, nil } func writeAgentFile(f string, v *agent.AuthInfo) error { data, err := json.MarshalIndent(v, "", "\t") if err != nil { return errgo.Mask(err) } data = append(data, '\n') // TODO should we write this atomically? if err := ioutil.WriteFile(f, data, 0600); err != nil { return errgo.Mask(err) } return nil } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/command_test.go000066400000000000000000000124761457263123000300700ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd_test import ( "bytes" "context" "encoding/pem" "io/ioutil" "net/http/httptest" "path/filepath" "testing" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" "github.com/juju/aclstore/v2" "github.com/juju/cmd/v3" "github.com/juju/simplekv/memsimplekv" "github.com/canonical/candid" "github.com/canonical/candid/candidtest" "github.com/canonical/candid/cmd/candid/internal/admincmd" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/static" internalcandidtest "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/store" "github.com/canonical/candid/store/memstore" ) type fixture struct { Dir string command cmd.Command aclStore aclstore.ACLStore store store.Store server *httptest.Server } func newFixture(c *qt.C) *fixture { f := new(fixture) adminAgentKey, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) f.aclStore = aclstore.NewACLStore(memsimplekv.NewStore()) f.store = memstore.NewStore() t, ok := c.TB.(candidtest.Testing) if !ok { t = &candidtestT{C: c} } f.server = candidtest.Serve(t, candid.ServerParams{ ACLStore: f.aclStore, Store: f.store, AdminAgentPublicKey: &adminAgentKey.Public, IdentityProviders: []idp.IdentityProvider{ static.NewIdentityProvider(static.Params{ Name: "static", }), }, }) c.Assert(err, qt.IsNil) c.Defer(func() { f.server.Close() }) f.Dir = c.Mkdir() // If the cookiejar gets saved, it gets saved to $HOME/.go-cookiejar, so make // sure that's not in the current directory. c.Setenv("HOME", f.Dir) c.Setenv("CANDID_URL", f.server.URL) err = admincmd.WriteAgentFile(filepath.Join(f.Dir, "admin.agent"), &agent.AuthInfo{ Key: adminAgentKey, Agents: []agent.Agent{{ URL: f.server.URL, Username: "admin@candid", }}, }) c.Assert(err, qt.IsNil) f.command = admincmd.New() internalcandidtest.LogTo(c) return f } func (s *fixture) CheckNoOutput(c *qt.C, args ...string) { stdout := s.CheckSuccess(c, args...) c.Assert(stdout, qt.Equals, "") } func (s *fixture) CheckSuccess(c *qt.C, args ...string) string { code, stdout, stderr := s.Run(args...) c.Assert(code, qt.Equals, 0, qt.Commentf("error code %d: (%s)", code, stderr)) c.Assert(stderr, qt.Equals, "", qt.Commentf("error code %d: (%s)", code, stderr)) return stdout } func (s *fixture) CheckError(c *qt.C, expectCode int, expectMessage string, args ...string) { code, stdout, stderr := s.Run(args...) c.Assert(code, qt.Equals, expectCode) c.Assert(stderr, qt.Matches, "(ERROR|error:) "+expectMessage+"\n") c.Assert(stdout, qt.Equals, "") } func (s *fixture) Run(args ...string) (code int, stdout, stderr string) { outbuf := new(bytes.Buffer) errbuf := new(bytes.Buffer) ctxt := &cmd.Context{ Dir: s.Dir, Stdin: bytes.NewReader(nil), Stdout: outbuf, Stderr: errbuf, } code = s.RunContext(ctxt, args...) return code, outbuf.String(), errbuf.String() } func (s *fixture) RunContext(ctxt *cmd.Context, args ...string) int { return cmd.Main(s.command, ctxt, args) } func TestLoadCACerts(t *testing.T) { c := qt.New(t) defer c.Done() ct, ok := c.TB.(candidtest.Testing) if !ok { ct = &candidtestT{C: c} } st := memstore.NewStore() adminAgentKey, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) srv := candidtest.ServeTLS(ct, candid.ServerParams{ Store: st, AdminAgentPublicKey: &adminAgentKey.Public, }) defer srv.Close() candidtest.AddIdentity(context.Background(), st, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", }) dir := c.Mkdir() c.Setenv("HOME", dir) c.Setenv("CANDID_URL", srv.URL) c.Setenv("BAKERY_AGENT_FILE", filepath.Join(dir, "admin.agent")) err = admincmd.WriteAgentFile(filepath.Join(dir, "admin.agent"), &agent.AuthInfo{ Key: adminAgentKey, Agents: []agent.Agent{{ URL: srv.URL, Username: "admin@candid", }}, }) c.Assert(err, qt.IsNil) certFile := filepath.Join(dir, "cacerts.pem") emptyFile := filepath.Join(dir, "empty.pem") unreadableFile := filepath.Join(dir, "unreadable.pem") nonExistentFile := filepath.Join(dir, "non-existent.pem") err = ioutil.WriteFile( certFile, pem.EncodeToMemory( &pem.Block{ Type: "CERTIFICATE", Bytes: srv.TLS.Certificates[0].Certificate[0], }, ), 0600, ) c.Assert(err, qt.IsNil) err = ioutil.WriteFile(emptyFile, nil, 0600) c.Assert(err, qt.IsNil) err = ioutil.WriteFile(unreadableFile, nil, 0) c.Assert(err, qt.IsNil) c.Setenv("CANDID_CA_CERTS", emptyFile+":"+unreadableFile+":"+nonExistentFile+"::"+certFile) outbuf := new(bytes.Buffer) errbuf := new(bytes.Buffer) ctxt := &cmd.Context{ Dir: dir, Stdin: bytes.NewReader(nil), Stdout: outbuf, Stderr: errbuf, } code := cmd.Main(admincmd.New(), ctxt, []string{"show", "-u", "bob"}) c.Assert(code, qt.Equals, 0, qt.Commentf("%s", errbuf.String())) c.Assert(outbuf.String(), qt.Equals, ` username: bob external-id: test:bob name: "" email: "" groups: [] ssh-keys: [] last-login: never last-discharge: never `[1:]) c.Assert(errbuf.String(), qt.Equals, "") } type candidtestT struct { *qt.C } func (t candidtestT) Cleanup(f func()) { t.Defer(f) } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/create-agent.go000066400000000000000000000115001457263123000277350ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd import ( "context" "encoding/json" "fmt" "os" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" "github.com/juju/cmd/v3" "github.com/juju/gnuflag" "gopkg.in/errgo.v1" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/params" ) type createAgentCommand struct { *candidCommand groups []string agentFile string agentFullName string admin bool parent bool publicKey *bakery.PublicKey } func newCreateAgentCommand(c *candidCommand) cmd.Command { return &createAgentCommand{ candidCommand: c, } } var createAgentDoc = ` The create-agent command creates an agent user on the Candid server. An agent user has an associated public key - the private key pair can be used to authenticate as that agent. The name of the agent is chosen by the identity manager itself and is written to the agent file, except as a special case, if the --admin flag is specified, when the agent information will only be written locally (this is so that an admin agent file can be generated before bootstrapping the Candid server for the first time). The agent will be made a member of any of the specified groups as long as the currently authenticated user is a member of those groups. A new key will be generated unless a key is specified with the -k flag or a key is found in the agent file (see below). If the --agent-file flag is specified, the specified file will be updated with the new agent information, otherwise the new agent information will be printed to the standard output. Note when the -k flag is specified, this information will be missing the private key. ` func (c *createAgentCommand) Info() *cmd.Info { return &cmd.Info{ Name: "create-agent", Args: "[group...]", Purpose: "create or update an agent user", Doc: createAgentDoc, } } func (c *createAgentCommand) SetFlags(f *gnuflag.FlagSet) { c.candidCommand.SetFlags(f) publicKeyVar(f, &c.publicKey, "k", "public key of agent") publicKeyVar(f, &c.publicKey, "public-key", "") f.StringVar(&c.agentFile, "f", "", "agent file to update") f.StringVar(&c.agentFile, "agent-file", "", "") f.BoolVar(&c.admin, "admin", false, "generate an agent file for the admin user; does not contact the identity manager service") f.StringVar(&c.agentFullName, "name", "", "name of agent") f.BoolVar(&c.parent, "parent", false, "create a parent agent") } func (c *createAgentCommand) Init(args []string) error { c.groups = args if c.agentFile != "" && c.publicKey != nil { return errgo.Newf("cannot specify public key and an agent file") } return errgo.Mask(c.candidCommand.Init(nil)) } func (c *createAgentCommand) Run(cmdctx *cmd.Context) error { defer c.Close(cmdctx) ctx := context.Background() client, err := c.Client(cmdctx) if err != nil { return errgo.Mask(err) } var key *bakery.KeyPair var agents *agent.AuthInfo if c.agentFile != "" { agents, err = readAgentFile(cmdctx.AbsPath(c.agentFile)) if err != nil { if !os.IsNotExist(errgo.Cause(err)) { return errgo.Mask(err) } agents = new(agent.AuthInfo) } else { key = agents.Key } } switch { case key == nil && c.publicKey == nil: key1, err := bakery.GenerateKey() if err != nil { return errgo.Notef(err, "cannot generate key") } key = key1 c.publicKey = &key.Public case c.publicKey == nil: c.publicKey = &key.Public } var username params.Username if c.admin { username = auth.AdminUsername if len(c.groups) > 0 { return errgo.Newf("cannot specify groups when using --admin flag") } } else { resp, err := client.CreateAgent(ctx, ¶ms.CreateAgentRequest{ CreateAgentBody: params.CreateAgentBody{ FullName: c.agentFullName, Groups: c.groups, PublicKeys: []*bakery.PublicKey{c.publicKey}, Parent: c.parent, }, }) if err != nil { return errgo.Mask(err) } username = resp.Username } if agents != nil { if agents.Key == nil { agents.Key = key } agents.Agents = append(agents.Agents, agent.Agent{ URL: client.Client.BaseURL, Username: string(username), }) if err := writeAgentFile(cmdctx.AbsPath(c.agentFile), agents); err != nil { return errgo.Mask(err) } fmt.Fprintf(cmdctx.Stdout, "added agent %s for %s to %s\n", username, client.Client.BaseURL, c.agentFile) return nil } agentsData := &agent.AuthInfo{ Agents: []agent.Agent{{ URL: client.Client.BaseURL, Username: string(username), }}, } if key != nil { agentsData.Key = key } else { agentsData.Key = &bakery.KeyPair{ Public: *c.publicKey, } } data, err := json.MarshalIndent(agentsData, "", "\t") if err != nil { return errgo.Mask(err) } data = append(data, '\n') cmdctx.Stdout.Write(data) return nil } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/create-agent_test.go000066400000000000000000000113341457263123000310010ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd_test import ( "context" "encoding/json" "path/filepath" "strings" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" "github.com/canonical/candid/cmd/candid/internal/admincmd" "github.com/canonical/candid/store" ) type createAgentSuite struct { fixture *fixture } func TestCreateAgent(t *testing.T) { qtsuite.Run(qt.New(t), &createAgentSuite{}) } func (s *createAgentSuite) Init(c *qt.C) { s.fixture = newFixture(c) } var createAgentUsageTests = []struct { about string args []string expectError string }{{ about: "agent file and agent key specified together", args: []string{"-k", "S2oglf2m3F7oN6o4d517Y/aRjObgw/S7ZNevIIp+NnQ=", "-f", "foo", "bob"}, expectError: `cannot specify public key and an agent file`, }, { about: "empty public key", args: []string{"-k", "", "bob"}, expectError: `invalid value "" for flag -k: wrong length for key, got 0 want 32`, }, { about: "invalid public key", args: []string{"-k", "xxx", "bob"}, expectError: `invalid value "xxx" for flag -k: wrong length for key, got 2 want 32`, }} func (s *createAgentSuite) TestUsage(c *qt.C) { for i, test := range createAgentUsageTests { c.Logf("test %d: %v", i, test.about) s.fixture.CheckError(c, 2, test.expectError, append([]string{"create-agent"}, test.args...)...) } } func (s *createAgentSuite) TestCreateAgentWithGeneratedKeyAndAgentFileNotSpecified(c *qt.C) { out := s.fixture.CheckSuccess(c, "create-agent", "--name", "agentname", "-a", "admin.agent") // The output should be valid input to an agent.AuthInfo unmarshal. var v agent.AuthInfo err := json.Unmarshal([]byte(out), &v) c.Assert(err, qt.IsNil) // Check that the public key looks right. agents := v.Agents c.Assert(agents, qt.HasLen, 1) c.Assert(agents[0].URL, qt.Equals, s.fixture.server.URL) identity := store.Identity{ Username: agents[0].Username, } c.Assert(s.fixture.store.Identity(context.Background(), &identity), qt.Equals, nil) c.Assert(identity.PublicKeys, qt.HasLen, 1) c.Assert(identity.PublicKeys[0], qt.Equals, v.Key.Public) } func (s *createAgentSuite) TestCreateAgentWithNonExistentAgentsFileSpecified(c *qt.C) { agentFile := filepath.Join(c.Mkdir(), ".agents") out := s.fixture.CheckSuccess(c, "create-agent", "-a", "admin.agent", "-f", agentFile) c.Assert(out, qt.Matches, `added agent a-[0-9a-f]+@candid for http://.* to .+\n`) v, err := admincmd.ReadAgentFile(agentFile) c.Assert(err, qt.IsNil) agents := v.Agents c.Assert(agents, qt.HasLen, 1) c.Assert(agents[0].URL, qt.Equals, s.fixture.server.URL) identity := store.Identity{ Username: agents[0].Username, } c.Assert(s.fixture.store.Identity(context.Background(), &identity), qt.Equals, nil) c.Assert(identity.PublicKeys, qt.HasLen, 1) c.Assert(identity.PublicKeys[0], qt.Equals, v.Key.Public) c.Assert(identity.Owner, qt.Equals, store.MakeProviderIdentity("idm", "admin")) } func (s *createAgentSuite) TestCreateAgentWithExistingAgentsFile(c *qt.C) { out := s.fixture.CheckSuccess(c, "create-agent", "-a", "admin.agent", "-f", "admin.agent", "somegroup") c.Assert(out, qt.Matches, `added agent a-[0-9a-f]+@candid for http://.* to .+\n`) v, err := admincmd.ReadAgentFile(filepath.Join(s.fixture.Dir, "admin.agent")) c.Assert(err, qt.IsNil) agents := v.Agents c.Assert(agents, qt.HasLen, 2) c.Assert(agents[1].URL, qt.Equals, s.fixture.server.URL) identity := store.Identity{ Username: agents[1].Username, } err = s.fixture.store.Identity(context.Background(), &identity) c.Assert(err, qt.IsNil) c.Assert(identity.Groups, qt.DeepEquals, []string{"somegroup"}) } func (s *createAgentSuite) TestCreateAgentWithAdminFlag(c *qt.C) { // With the -n flag, it doesn't contact the candid server at all. out := s.fixture.CheckSuccess(c, "create-agent", "--admin") var v agent.AuthInfo err := json.Unmarshal([]byte(out), &v) c.Assert(err, qt.IsNil) agents := v.Agents c.Assert(agents, qt.HasLen, 1) c.Assert(agents[0].Username, qt.Equals, "admin@candid") c.Assert(agents[0].URL, qt.Equals, s.fixture.server.URL) } func (s *createAgentSuite) TestCreateAgentWithParentFlag(c *qt.C) { // With the -n flag, it doesn't contact the candid server at all. out := s.fixture.CheckSuccess(c, "create-agent", "-a", "admin.agent", "--parent") var v agent.AuthInfo err := json.Unmarshal([]byte(out), &v) c.Assert(err, qt.IsNil) agents := v.Agents c.Assert(agents, qt.HasLen, 1) if !strings.HasPrefix(string(agents[0].Username), "a-") { c.Errorf("unexpected agent username %q", agents[0].Username) } c.Assert(agents[0].URL, qt.Equals, s.fixture.server.URL) } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/export_test.go000066400000000000000000000002711457263123000277610ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd var ( WriteAgentFile = writeAgentFile ReadAgentFile = readAgentFile ) golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/find.go000066400000000000000000000106141457263123000263230ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd import ( "context" "fmt" "io" "strings" "time" "github.com/juju/cmd/v3" "github.com/juju/gnuflag" "gopkg.in/errgo.v1" "github.com/canonical/candid/params" ) type findCommand struct { *candidCommand out cmd.Output detail string email string lastLoginDays uint lastDischargeDays uint } func newFindCommand(c *candidCommand) cmd.Command { return &findCommand{ candidCommand: c, } } var findDoc = ` The find command finds users that match the request parameters. candid find -e bob@example.com candid find --last-login=30 ` func (c *findCommand) Info() *cmd.Info { return &cmd.Info{ Name: "find", Purpose: "find users", Doc: findDoc, } } func (c *findCommand) SetFlags(f *gnuflag.FlagSet) { c.candidCommand.SetFlags(f) c.out.AddFlags(f, "tab", map[string]cmd.Formatter{ "yaml": cmd.FormatYaml, "json": cmd.FormatJson, "smart": cmd.FormatSmart, "tab": c.formatTab, }) f.StringVar(&c.detail, "d", "", "include user details, comma separated list of external_id, email, gravatar_id, or fullname output is forced to tab separated") f.StringVar(&c.email, "e", "", "email address of the user") f.StringVar(&c.email, "email", "", "") f.UintVar(&c.lastLoginDays, "last-login", 0, "users whose last successful login was within this number of days") f.UintVar(&c.lastDischargeDays, "last-discharge", 0, "users whose last successful discharge was within this number of days") } func (c *findCommand) Init(args []string) error { return errgo.Mask(c.candidCommand.Init(nil)) } func (c *findCommand) Run(ctxt *cmd.Context) error { defer c.Close(ctxt) client, err := c.candidCommand.Client(ctxt) if err != nil { return errgo.Mask(err) } req := params.QueryUsersRequest{ Email: c.email, } if c.lastLoginDays > 0 { req.LastLoginSince = daysAgo(c.lastLoginDays) } if c.lastDischargeDays > 0 { req.LastDischargeSince = daysAgo(c.lastDischargeDays) } usernames, err := client.QueryUsers(context.Background(), &req) if err != nil { return errgo.Mask(err) } if "" == c.detail { return c.out.Write(ctxt, usernames) } fields := strings.Split(c.detail, ",") var user_output []map[string]string for _, u := range usernames { user_out := make(map[string]string) user_out["username"] = u user, err2 := client.User(context.Background(), ¶ms.UserRequest{ Username: params.Username(u), }) if err2 != nil { fmt.Fprintf(ctxt.Stderr, "%v ... continuing\n", err2) } for _, f := range fields { switch strings.ToLower(strings.Trim(f, " ")) { case "email": user_out["email"] = user.Email case "external_id": user_out["external_id"] = user.ExternalID case "fullname": user_out["fullname"] = user.FullName case "gravatar_id": user_out["gravatar_id"] = user.GravatarID } } user_output = append(user_output, user_out) } return c.out.Write(ctxt, user_output) } // daysAgo returns the current time less the given // number of days, formatted as a string as required // by time fields in params.QueryUsersRequest. func daysAgo(days uint) string { t := time.Now().AddDate(0, 0, -int(days)) b, err := t.MarshalText() if err != nil { // This should be impossible unless things are severly wrong. panic(err) } return string(b) } func (c *findCommand) formatTab(writer io.Writer, value interface{}) error { users, ok := value.([]map[string]string) if ok { return c.formatTabMap(writer, users) } userl, ok := value.([]string) if ok { return c.formatTabSlice(writer, userl) } return nil } func (c *findCommand) formatTabMap(writer io.Writer, users []map[string]string) error { fields := []string{"username"} for _, f := range strings.Split(c.detail, ",") { fields = append(fields, f) } i := 0 s := len(fields) for _, k := range fields { io.WriteString(writer, k) if i < s { io.WriteString(writer, "\t") } i++ } io.WriteString(writer, "\n") ul := len(users) for j, u := range users { i = 0 s = len(users[0]) for _, k := range fields { if u[k] == "" { u[k] = "-" } io.WriteString(writer, u[k]) if i < s { io.WriteString(writer, "\t") } i++ } if j < ul { io.WriteString(writer, "\n") } } return nil } func (c *findCommand) formatTabSlice(writer io.Writer, users []string) error { for _, u := range users { io.WriteString(writer, u) } return nil } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/find_test.go000066400000000000000000000136711457263123000273700ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd_test import ( "context" "encoding/json" "testing" "time" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/canonical/candid/candidtest" "github.com/canonical/candid/store" ) type findSuite struct { fixture *fixture } func TestFind(t *testing.T) { qtsuite.Run(qt.New(t), &findSuite{}) } func (s *findSuite) Init(c *qt.C) { s.fixture = newFixture(c) } func (s *findSuite) TestFindEmail(c *qt.C) { ctx := context.Background() candidtest.AddIdentity(ctx, s.fixture.store, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", Email: "bob@example.com", }) stdout := s.fixture.CheckSuccess(c, "find", "-a", "admin.agent", "-e", "bob@example.com") c.Assert(stdout, qt.Equals, "bob\n") } func (s *findSuite) TestFindEmailNotFound(c *qt.C) { stdout := s.fixture.CheckSuccess(c, "find", "-a", "admin.agent", "-e", "bob@example.com") c.Assert(stdout, qt.Equals, "\n") } func (s *findSuite) TestFindNoParameters(c *qt.C) { ctx := context.Background() identites := []store.Identity{{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", Email: "bob@example.com", }, { ProviderID: store.MakeProviderIdentity("test", "alice"), Username: "alice", }, { ProviderID: store.MakeProviderIdentity("test", "charlie"), Username: "charlie", }} for _, id := range identites { candidtest.AddIdentity(ctx, s.fixture.store, &id) } stdout := s.fixture.CheckSuccess(c, "find", "-a", "admin.agent", "--format", "json") var usernames []string err := json.Unmarshal([]byte(stdout), &usernames) c.Assert(err, qt.IsNil) c.Assert(usernames, qt.DeepEquals, []string{"admin@candid", "alice", "bob", "charlie"}) } func (s *findSuite) TestFindLastLoginTime(c *qt.C) { ctx := context.Background() identities := []store.Identity{{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", Email: "bob@example.com", LastLogin: time.Now().Add(-31 * 24 * time.Hour), }, { ProviderID: store.MakeProviderIdentity("test", "alice"), Username: "alice", LastLogin: time.Now().Add(-10 * 24 * time.Hour), }, { ProviderID: store.MakeProviderIdentity("test", "charlie"), Username: "charlie", LastLogin: time.Now().Add(-1 * 24 * time.Hour), }} for _, id := range identities { candidtest.AddIdentity(ctx, s.fixture.store, &id) } stdout := s.fixture.CheckSuccess(c, "find", "-a", "admin.agent", "--format", "json", "--last-login", "30") var usernames []string err := json.Unmarshal([]byte(stdout), &usernames) c.Assert(err, qt.IsNil) c.Assert(usernames, qt.DeepEquals, []string{"alice", "charlie"}) } func (s *findSuite) TestFindLastDischargeTime(c *qt.C) { ctx := context.Background() identities := []store.Identity{{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", Email: "bob@example.com", LastDischarge: time.Now().Add(-31 * 24 * time.Hour), }, { ProviderID: store.MakeProviderIdentity("test", "alice"), Username: "alice", LastDischarge: time.Now().Add(-10 * 24 * time.Hour), }, { ProviderID: store.MakeProviderIdentity("test", "charlie"), Username: "charlie", LastDischarge: time.Now().Add(-1 * 24 * time.Hour), }} for _, id := range identities { candidtest.AddIdentity(ctx, s.fixture.store, &id) } stdout := s.fixture.CheckSuccess(c, "find", "-a", "admin.agent", "--format", "json", "--last-discharge", "20") var usernames []string err := json.Unmarshal([]byte(stdout), &usernames) c.Assert(err, qt.IsNil) c.Assert(usernames, qt.DeepEquals, []string{"admin@candid", "alice", "charlie"}) } func (s *findSuite) TestFindWithEmail(c *qt.C) { ctx := context.Background() identities := []store.Identity{{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", Email: "bob@example.com", }, { ProviderID: store.MakeProviderIdentity("test", "alice"), Username: "alice", Email: "alice@example.com", }, { ProviderID: store.MakeProviderIdentity("test", "charlie"), Username: "charlie", Email: "charlie@example.com", }} for _, id := range identities { candidtest.AddIdentity(ctx, s.fixture.store, &id) } stdout := s.fixture.CheckSuccess(c, "find", "-a", "admin.agent", "-d", "email", "--format", "json") var usernames []map[string]string err := json.Unmarshal([]byte(stdout), &usernames) c.Assert(err, qt.IsNil) c.Assert(usernames, qt.DeepEquals, []map[string]string{ {"username": "admin@candid", "email": ""}, {"username": "alice", "email": "alice@example.com"}, {"username": "bob", "email": "bob@example.com"}, {"username": "charlie", "email": "charlie@example.com"}, }) } func (s *findSuite) TestFindWithEmailAndGravatar(c *qt.C) { ctx := context.Background() identities := []store.Identity{{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", Email: "bob@example.com", }, { ProviderID: store.MakeProviderIdentity("test", "alice"), Username: "alice", Email: "alice@example.com", }, { ProviderID: store.MakeProviderIdentity("test", "charlie"), Username: "charlie", Email: "charlie@example.com", }} for _, id := range identities { candidtest.AddIdentity(ctx, s.fixture.store, &id) } stdout := s.fixture.CheckSuccess(c, "find", "-a", "admin.agent", "-d", "email, gravatar_id", "--format", "json") var usernames []map[string]string err := json.Unmarshal([]byte(stdout), &usernames) c.Assert(err, qt.IsNil) c.Assert(usernames, qt.DeepEquals, []map[string]string{ {"username": "admin@candid", "email": "", "gravatar_id": ""}, {"username": "alice", "email": "alice@example.com", "gravatar_id": "c160f8cc69a4f0bf2b0362752353d060"}, {"username": "bob", "email": "bob@example.com", "gravatar_id": "4b9bb80620f03eb3719e0a061c14283d"}, {"username": "charlie", "email": "charlie@example.com", "gravatar_id": "426b189df1e2f359efe6ee90f2d2030f"}, }) } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/remove-group.go000066400000000000000000000030101457263123000300220ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd import ( "context" "github.com/juju/cmd/v3" "gopkg.in/errgo.v1" "github.com/canonical/candid/params" ) type removeGroupCommand struct { userCommand groups []string } func newRemoveGroupCommand(cc *candidCommand) cmd.Command { c := &removeGroupCommand{} c.candidCommand = cc return c } var removeGroupDoc = ` The remove-group command removes the specified user from the specified group, or groups. To remove the group-1 and group-2 groups from the user bob: candid remove-group -u bob group-1 group-2 To remove the example-1 and example-2 groups from the user with the email removeress bob@example.com: candid remove-group -e bob@example.com group-1 group-2 ` func (c *removeGroupCommand) Info() *cmd.Info { return &cmd.Info{ Name: "remove-group", Args: "[group...]", Purpose: "remove a user from groups", Doc: removeGroupDoc, } } func (c *removeGroupCommand) Init(args []string) error { c.groups = args return errgo.Mask(c.userCommand.Init(nil)) } func (c *removeGroupCommand) Run(ctxt *cmd.Context) error { defer c.Close(ctxt) username, err := c.lookupUser(ctxt) if err != nil { return errgo.Mask(err) } client, err := c.Client(ctxt) if err != nil { return errgo.Mask(err) } err = client.ModifyUserGroups(context.Background(), ¶ms.ModifyUserGroupsRequest{ Username: username, Groups: params.ModifyGroups{ Remove: c.groups, }, }) return errgo.Mask(err) } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/remove-group_test.go000066400000000000000000000042751457263123000310770ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd_test import ( "context" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/canonical/candid/candidtest" "github.com/canonical/candid/store" ) type removeGroupSuite struct { fixture *fixture } func TestRemoveGroup(t *testing.T) { qtsuite.Run(qt.New(t), &removeGroupSuite{}) } func (s *removeGroupSuite) Init(c *qt.C) { s.fixture = newFixture(c) } func (s *removeGroupSuite) TestRemoveGroup(c *qt.C) { ctx := context.Background() candidtest.AddIdentity(ctx, s.fixture.store, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", Groups: []string{"test1", "test2", "test3"}, }) s.fixture.CheckNoOutput(c, "remove-group", "-a", "admin.agent", "-u", "bob", "test1", "test2") identity := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), } err := s.fixture.store.Identity(ctx, &identity) c.Assert(err, qt.IsNil) c.Assert(identity.Groups, qt.DeepEquals, []string{"test3"}) } func (s *removeGroupSuite) TestRemoveGroupForEmail(c *qt.C) { ctx := context.Background() candidtest.AddIdentity(ctx, s.fixture.store, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", Email: "bob@example.com", Groups: []string{"test1", "test2", "test3"}, }) s.fixture.CheckNoOutput(c, "remove-group", "-a", "admin.agent", "-e", "bob@example.com", "test1", "test2") identity := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), } err := s.fixture.store.Identity(ctx, &identity) c.Assert(err, qt.IsNil) c.Assert(identity.Groups, qt.DeepEquals, []string{"test3"}) } func (s *removeGroupSuite) TestRemoveGroupForEmailNotFound(c *qt.C) { s.fixture.CheckError( c, 1, `no user found for email "alice@example.com"`, "remove-group", "-a", "admin.agent", "-e", "alice@example.com", "test1", "test2", ) } func (s *removeGroupSuite) TestRemoveGroupNoUser(c *qt.C) { s.fixture.CheckError( c, 2, `no user specified, please specify either username or email`, "remove-group", "-a", "admin.agent", "test1", "test2", ) } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/show.go000066400000000000000000000053311457263123000263630ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd import ( "context" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/cmd/v3" "github.com/juju/gnuflag" "gopkg.in/errgo.v1" "github.com/canonical/candid/params" ) type showCommand struct { userCommand out cmd.Output } func newShowCommand(cc *candidCommand) cmd.Command { c := &showCommand{} c.candidCommand = cc return c } var showDoc = ` The show command shows the details for the specified user. candid show -e bob@example.com ` func (c *showCommand) Info() *cmd.Info { return &cmd.Info{ Name: "show", Purpose: "show user details", Doc: showDoc, } } func (c *showCommand) SetFlags(f *gnuflag.FlagSet) { c.userCommand.SetFlags(f) c.out.AddFlags(f, "smart", cmd.DefaultFormatters.Formatters()) } func (c *showCommand) Run(ctxt *cmd.Context) error { defer c.Close(ctxt) ctx := context.Background() username, err := c.lookupUser(ctxt) if err != nil { return errgo.Mask(err) } client, err := c.Client(ctxt) if err != nil { return errgo.Mask(err) } u, err := client.User(ctx, ¶ms.UserRequest{ Username: username, }) if err != nil { return errgo.Mask(err) } user := user{ Username: string(u.Username), ExternalID: u.ExternalID, Owner: string(u.Owner), Groups: []string{}, SSHKeys: []string{}, LastLogin: timeString(u.LastLogin), LastDischarge: timeString(u.LastDischarge), } if u.ExternalID != "" { user.Name = &u.FullName user.Email = &u.Email } else { user.PublicKeys = u.PublicKeys } if len(u.IDPGroups) > 0 { user.Groups = u.IDPGroups } if len(u.SSHKeys) > 0 { user.SSHKeys = u.SSHKeys } return c.out.Write(ctxt, user) } func timeString(t *time.Time) string { if t == nil || t.IsZero() { return "never" } return t.Format(time.RFC3339) } // user represents a user in the system. type user struct { Username string `json:"username" yaml:"username"` ExternalID string `json:"external-id,omitempty" yaml:"external-id,omitempty"` Name *string `json:"name,omitempty" yaml:"name,omitempty"` Email *string `json:"email,omitempty" yaml:"email,omitempty"` Owner string `json:"owner,omitempty" yaml:"owner,omitempty"` PublicKeys []*bakery.PublicKey `json:"public-keys,omitempty" yaml:"public-keys,omitempty"` Groups []string `json:"groups" yaml:"groups"` SSHKeys []string `json:"ssh-keys" yaml:"ssh-keys"` LastLogin string `json:"last-login" yaml:"last-login"` LastDischarge string `json:"last-discharge" yaml:"last-discharge"` } golang-github-canonical-candid-1.12.3/cmd/candid/internal/admincmd/show_test.go000066400000000000000000000133431457263123000274240ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package admincmd_test import ( "context" "path/filepath" "testing" "time" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/canonical/candid/candidtest" "github.com/canonical/candid/store" ) type showSuite struct { fixture *fixture } func TestShow(t *testing.T) { qtsuite.Run(qt.New(t), &showSuite{}) } func (s *showSuite) Init(c *qt.C) { s.fixture = newFixture(c) } func (s *showSuite) TestShowUserWithAgentEnv(c *qt.C) { // This test acts as a proxy agent-env functionality in all the // other command that use NewClient. c.Setenv("BAKERY_AGENT_FILE", filepath.Join(s.fixture.Dir, "admin.agent")) ctx := context.Background() candidtest.AddIdentity(ctx, s.fixture.store, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", }) stdout := s.fixture.CheckSuccess(c, "show", "-u", "bob") c.Assert(stdout, qt.Equals, ` username: bob external-id: test:bob name: "" email: "" groups: [] ssh-keys: [] last-login: never last-discharge: never `[1:]) } func (s *showSuite) TestShowUser(c *qt.C) { ctx := context.Background() candidtest.AddIdentity(ctx, s.fixture.store, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", Name: "Bob Robertson", Email: "bob@example.com", Groups: []string{"g1", "g2"}, LastLogin: time.Date(2016, 12, 25, 0, 0, 0, 0, time.UTC), LastDischarge: time.Date(2016, 12, 25, 0, 0, 0, 0, time.UTC), ExtraInfo: map[string][]string{ "sshkeys": {"key1", "key2"}, }, }) stdout := s.fixture.CheckSuccess(c, "show", "-a", "admin.agent", "-u", "bob") c.Assert(stdout, qt.Equals, ` username: bob external-id: test:bob name: Bob Robertson email: bob@example.com groups: - g1 - g2 ssh-keys: - key1 - key2 last-login: "2016-12-25T00:00:00Z" last-discharge: "2016-12-25T00:00:00Z" `[1:]) } func (s *showSuite) TestShowEmail(c *qt.C) { ctx := context.Background() candidtest.AddIdentity(ctx, s.fixture.store, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", Name: "Bob Robertson", Email: "bob@example.com", Groups: []string{"g1", "g2"}, LastLogin: time.Date(2016, 12, 25, 0, 0, 0, 0, time.UTC), LastDischarge: time.Date(2016, 12, 25, 0, 0, 0, 0, time.UTC), ExtraInfo: map[string][]string{ "sshkeys": {"key1", "key2"}, }, }) stdout := s.fixture.CheckSuccess(c, "show", "-a", "admin.agent", "-e", "bob@example.com") c.Assert(stdout, qt.Equals, ` username: bob external-id: test:bob name: Bob Robertson email: bob@example.com groups: - g1 - g2 ssh-keys: - key1 - key2 last-login: "2016-12-25T00:00:00Z" last-discharge: "2016-12-25T00:00:00Z" `[1:]) } func (s *showSuite) TestShowEmailNotFound(c *qt.C) { s.fixture.CheckError( c, 1, `no user found for email "bob@example.com"`, "show", "-a", "admin.agent", "-e", "bob@example.com", ) } func (s *showSuite) TestShowNoParameters(c *qt.C) { s.fixture.CheckError( c, 2, `no user specified, please specify either username or email`, "show", ) } func (s *showSuite) TestShowAgentUser(c *qt.C) { ctx := context.Background() var pk bakery.PublicKey identities := []store.Identity{{ ProviderID: store.MakeProviderIdentity("static", "alice"), Username: "alice", Groups: []string{"g1", "g2"}, }, { ProviderID: store.MakeProviderIdentity("idm", "a-1234"), Username: "a-1234@candid", PublicKeys: []bakery.PublicKey{pk}, Groups: []string{"g1", "g2"}, Owner: store.MakeProviderIdentity("static", "alice"), LastLogin: time.Date(2016, 12, 25, 0, 0, 0, 0, time.UTC), LastDischarge: time.Date(2016, 12, 25, 0, 0, 0, 0, time.UTC), }} for _, id := range identities { candidtest.AddIdentity(ctx, s.fixture.store, &id) } stdout := s.fixture.CheckSuccess(c, "show", "-a", "admin.agent", "-u", "a-1234@candid") c.Assert(stdout, qt.Equals, ` username: a-1234@candid owner: alice public-keys: - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= groups: - g1 - g2 ssh-keys: [] last-login: "2016-12-25T00:00:00Z" last-discharge: "2016-12-25T00:00:00Z" `[1:]) } func (s *showSuite) TestShowZeroValues(c *qt.C) { ctx := context.Background() candidtest.AddIdentity(ctx, s.fixture.store, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", }) stdout := s.fixture.CheckSuccess(c, "show", "-a", "admin.agent", "-u", "bob") c.Assert(stdout, qt.Equals, ` username: bob external-id: test:bob name: "" email: "" groups: [] ssh-keys: [] last-login: never last-discharge: never `[1:]) } func (s *showSuite) TestShowUserError(c *qt.C) { s.fixture.CheckError( c, 1, `Get http://.*/v1/u/bob: user bob not found`, "show", "-a", "admin.agent", "-u", "bob", ) } func (s *showSuite) TestShowUserJSON(c *qt.C) { ctx := context.Background() candidtest.AddIdentity(ctx, s.fixture.store, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "bob"), Username: "bob", Name: "Bob Robertson", Email: "bob@example.com", Groups: []string{"g1", "g2"}, LastLogin: time.Date(2016, 12, 25, 0, 0, 0, 0, time.UTC), LastDischarge: time.Date(2016, 12, 25, 0, 0, 0, 0, time.UTC), ExtraInfo: map[string][]string{ "sshkeys": {"key1", "key2"}, }, }) stdout := s.fixture.CheckSuccess(c, "show", "-a", "admin.agent", "-u", "bob", "--format", "json") c.Assert(stdout, qt.Equals, ` {"username":"bob","external-id":"test:bob","name":"Bob Robertson","email":"bob@example.com","groups":["g1","g2"],"ssh-keys":["key1","key2"],"last-login":"2016-12-25T00:00:00Z","last-discharge":"2016-12-25T00:00:00Z"} `[1:]) } golang-github-canonical-candid-1.12.3/cmd/candid/main.go000066400000000000000000000006021457263123000227330ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package main import ( "os" "github.com/juju/cmd/v3" "github.com/canonical/candid/cmd/candid/internal/admincmd" ) func main() { ctxt := &cmd.Context{ Dir: ".", Stdout: os.Stdout, Stderr: os.Stderr, Stdin: os.Stdin, } os.Exit(cmd.Main(admincmd.New(), ctxt, os.Args[1:])) } golang-github-canonical-candid-1.12.3/cmd/candidsrv/000077500000000000000000000000001457263123000222155ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/cmd/candidsrv/admin.agent000066400000000000000000000002651457263123000243300ustar00rootroot00000000000000{"url":"https://localhost:8081","username":"admin@candid","public_key":"iKp24EL2Aj9MQfRkpwzp7rz7Zf3QZsEzWGpoWT3OK2w=","private_key":"shThrvZU0bLuEdkr67M6SiuX6GRxbFM2B1CKVsLIiiA="} golang-github-canonical-candid-1.12.3/cmd/candidsrv/config.yaml000066400000000000000000000023731457263123000243530ustar00rootroot00000000000000storage: type: memory listen-address: :8081 auth-username: admin auth-password: password location: 'http://localhost:8081' private-key: 8PjzjakvIlh3BVFKe8axinRDutF6EDIfjtuf4+JaNow= public-key: CIdWcEUN+0OZnKW9KwruRQnQDY/qqzVdD30CijwiWCk= access-log: access.log max-mgo-sessions: 300 request-timeout: 2s private-addr: localhost admin-agent-public-key: iKp24EL2Aj9MQfRkpwzp7rz7Zf3QZsEzWGpoWT3OK2w= mfa-rp-display-name: Candid mfa-rp-id: localhost mfa-rp-origin: http://localhost:8081 identity-providers: - type: static name: static description: Default identity provider require-mfa: true users: user1: name: User One email: user1@example.com password: password1 groups: - group1 - group3 user2: name: User Two email: user2@example.com password: password2 groups: - group2 - group3 - type: static name: static-domain domain: domain description: Identity provider @domain users: user1: name: User One email: user1@example.com password: password3 groups: - group1 - group3 user2: name: User Two email: user2@example.com password: password4 groups: - group2 - group3 enable-email-login: true golang-github-canonical-candid-1.12.3/cmd/candidsrv/main.go000066400000000000000000000136271457263123000235010ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package main import ( "flag" "fmt" "net/http" "net/url" "os" "path/filepath" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/gorilla/handlers" "github.com/juju/loggo" _ "github.com/lib/pq" "golang.org/x/net/http/httpproxy" "gopkg.in/errgo.v1" "gopkg.in/natefinch/lumberjack.v2" "github.com/canonical/candid" "github.com/canonical/candid/config" "github.com/canonical/candid/idp" _ "github.com/canonical/candid/idp/adfs" _ "github.com/canonical/candid/idp/agent" _ "github.com/canonical/candid/idp/azure" _ "github.com/canonical/candid/idp/google" _ "github.com/canonical/candid/idp/keycloak" _ "github.com/canonical/candid/idp/keystone" _ "github.com/canonical/candid/idp/ldap" _ "github.com/canonical/candid/idp/static" "github.com/canonical/candid/idp/usso" _ "github.com/canonical/candid/idp/usso/ussodischarge" _ "github.com/canonical/candid/idp/usso/ussooauth" "github.com/canonical/candid/internal/mfa" _ "github.com/canonical/candid/store/memstore" _ "github.com/canonical/candid/store/mgostore" _ "github.com/canonical/candid/store/sqlstore" ) var logger = loggo.GetLogger("candidsrv") func main() { flag.Usage = func() { fmt.Fprintf(os.Stderr, "usage: %s [options] \n", filepath.Base(os.Args[0])) flag.PrintDefaults() exit(2) } flag.Parse() if flag.NArg() != 1 { flag.Usage() } confPath := flag.Arg(0) conf, err := config.Read(confPath) if err != nil { fmt.Fprintf(os.Stderr, "STOP cannot read configuration: %v\n", err) exit(2) } if err := loggo.ConfigureLoggers(conf.LoggingConfig); err != nil { fmt.Fprintf(os.Stderr, "STOP cannot configure loggers: %v", err) exit(2) } if err := serve(conf); err != nil { fmt.Fprintf(os.Stderr, "STOP %v\n", err) exit(1) } fmt.Fprintln(os.Stderr, "STOP no error, weirdly") exit(0) } // exit calls os.Exit, first sleeping for a bit to work // around an outrageous systemd bug which causes // final output lines to be lost if we exit immediately. // See https://github.com/systemd/systemd/issues/2913 // // Note: exit status 2 implies we won't restart the service. func exit(code int) { time.Sleep(200 * time.Millisecond) os.Exit(code) } // serve starts the identity service. func serve(conf *config.Config) error { if conf.HTTPProxy != "" { logger.Infof("configuring HTTP(S) proxy %s", conf.HTTPProxy) t, ok := http.DefaultTransport.(*http.Transport) if !ok { return errgo.New("http-proxy configured, but DefaultTransport cannot be modified.") } pcfg := httpproxy.Config{ HTTPProxy: conf.HTTPProxy, HTTPSProxy: conf.HTTPProxy, NoProxy: conf.NoProxy, } pf := pcfg.ProxyFunc() t.Proxy = func(req *http.Request) (*url.URL, error) { return pf(req.URL) } } backend, err := conf.Storage.NewBackend() if err != nil { return errgo.Mask(err) } defer backend.Close() return serveIdentity(conf, candid.ServerParams{ Store: backend.Store(), ProviderDataStore: backend.ProviderDataStore(), MeetingStore: backend.MeetingStore(), RootKeyStore: backend.BakeryRootKeyStore(), DebugStatusCheckerFuncs: backend.DebugStatusCheckerFuncs(), ACLStore: backend.ACLStore(), }) } func serveIdentity(conf *config.Config, params candid.ServerParams) error { logger.Infof("setting up the identity server") params.IdentityProviders = defaultIDPs if len(conf.IdentityProviders) > 0 { params.IdentityProviders = make([]idp.IdentityProvider, len(conf.IdentityProviders)) for i, idp := range conf.IdentityProviders { params.IdentityProviders[i] = idp.IdentityProvider } } params.StaticFileSystem = staticFS(conf.ResourcePath) var err error params.Template, err = loadTemplates(conf.ResourcePath) if err != nil { return errgo.Notef(err, "cannot parse templates") } if conf.MFARPDisplayName != "" && conf.MFARPID != "" && conf.MFARPOrigin != "" { authenticator, err := mfa.NewAuthenticator(conf.MFARPID, conf.MFARPDisplayName, conf.MFARPOrigin) if err != nil { return errgo.Mask(err) } params.MFAAuthenticator = authenticator } else { logger.Infof("multi-factor authentication not enabled") } params.AdminPassword = conf.AdminPassword params.Key = &bakery.KeyPair{ Private: *conf.PrivateKey, Public: *conf.PublicKey, } params.RendezvousTimeout = conf.RendezvousTimeout.Duration params.Location = conf.Location params.PrivateAddr = conf.PrivateAddr params.AdminAgentPublicKey = conf.AdminAgentPublicKey params.RedirectLoginTrustedURLs = conf.RedirectLoginTrustedURLs params.RedirectLoginTrustedDomains = conf.RedirectLoginTrustedDomains params.APIMacaroonTimeout = conf.APIMacaroonTimeout.Duration params.DischargeMacaroonTimeout = conf.DischargeMacaroonTimeout.Duration params.DischargeTokenTimeout = conf.DischargeTokenTimeout.Duration params.SkipLocationForCookiePaths = conf.SkipLocationForCookiePaths params.EnableEmailLogin = conf.EnableEmailLogin srv, err := candid.NewServer( params, candid.V1, candid.Debug, candid.Discharger, ) if err != nil { return errgo.Notef(err, "cannot create new server at %q", conf.ListenAddress) } defer srv.Close() // Cast the Server to an http.Handler so that it can be // optionally wrapped by the logging handler below. var server http.Handler = srv if conf.AccessLog != "" { accesslog := &lumberjack.Logger{ Filename: conf.AccessLog, MaxSize: 500, // megabytes MaxBackups: 3, MaxAge: 28, //days } server = handlers.CombinedLoggingHandler(accesslog, server) } logger.Infof("starting the identity server") httpServer := &http.Server{ Addr: conf.ListenAddress, Handler: server, TLSConfig: conf.TLSConfig(), } fmt.Println("START") if conf.TLSConfig() != nil { return httpServer.ListenAndServeTLS("", "") } return httpServer.ListenAndServe() } var defaultIDPs = []idp.IdentityProvider{ usso.NewIdentityProvider(usso.Params{}), } golang-github-canonical-candid-1.12.3/cmd/candidsrv/resources.go000066400000000000000000000007031457263123000245560ustar00rootroot00000000000000// Copyright 2021 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // +build !go1.16 package main import ( "html/template" "net/http" "path/filepath" ) func loadTemplates(resourcePath string) (*template.Template, error) { return template.New("").ParseGlob(filepath.Join(resourcePath, "templates", "*")) } func staticFS(resourcePath string) http.FileSystem { return http.Dir(filepath.Join(resourcePath, "static")) } golang-github-canonical-candid-1.12.3/cmd/candidsrv/resources_go116.go000066400000000000000000000014541457263123000254770ustar00rootroot00000000000000// Copyright 2021 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // +build go1.16 package main import ( "html/template" "io/fs" "net/http" "path/filepath" "github.com/canonical/candid" ) func loadTemplates(resourcePath string) (*template.Template, error) { if resourcePath == "" { templateFS, err := fs.Sub(candid.ResourceFS, "templates") if err != nil { panic(err) } return template.New("").ParseFS(templateFS, "*") } return template.New("").ParseGlob(filepath.Join(resourcePath, "templates", "*")) } func staticFS(resourcePath string) http.FileSystem { if resourcePath == "" { staticFS, err := fs.Sub(candid.ResourceFS, "static") if err != nil { panic(err) } return http.FS(staticFS) } return http.Dir(filepath.Join(resourcePath, "static")) } golang-github-canonical-candid-1.12.3/cmd/migrate-db/000077500000000000000000000000001457263123000222535ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/cmd/migrate-db/internal/000077500000000000000000000000001457263123000240675ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/cmd/migrate-db/internal/legacy.go000066400000000000000000000066171457263123000256740ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package internal import ( "log" "strings" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" mgo "github.com/juju/mgo/v2" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/cmd/migrate-db/internal/mongodoc" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/store" ) const ( legacyAdminGroup = "admin@idm" legacyGroupListGroup = "grouplist@idm" legacySSHKeyGetterGroup = "sshkeygetter@idm" ) // A LegacySource is a Source from a legacy mgo store. type LegacySource struct { db *mgo.Database identity *store.Identity iter *mgo.Iter err error } // NewLegacySource creates a LegacySource from the given database. func NewLegacySource(db *mgo.Database) *LegacySource { return &LegacySource{ db: db, } } // Next implements Source.Next. func (s *LegacySource) Next() bool { if s.iter == nil { s.iter = s.db.C("identities").Find(nil).Iter() } for { var doc mongodoc.Identity if !s.iter.Next(&doc) { return false } var err error if doc.Username == legacyAdminGroup { continue } s.identity, err = convert(&doc) if err != nil { log.Printf("cannot convert identity (skipping): %s", err) continue } return true } } func convert(doc *mongodoc.Identity) (*store.Identity, error) { providerID := providerID(doc) if providerID == "" { return nil, errgo.Newf("unrecognised external ID %q", doc.ExternalID) } identity := &store.Identity{ Username: doc.Username, ProviderID: providerID, Name: doc.FullName, Email: doc.Email, Groups: doc.Groups, } if doc.LastLogin != nil { identity.LastLogin = *doc.LastLogin } if doc.LastDischarge != nil { identity.LastDischarge = *doc.LastDischarge } for _, k := range doc.PublicKeys { var key bakery.Key copy(key[:], k.Key) identity.PublicKeys = append(identity.PublicKeys, bakery.PublicKey{key}) } if doc.Owner != "" { if doc.Owner == legacyAdminGroup { identity.Owner = auth.AdminProviderID } else { return nil, errgo.Newf("unrecognised owner for %s (%q)", doc.Username, doc.Owner) } } if len(doc.SSHKeys) > 0 { identity.ExtraInfo = map[string][]string{ "sshkeys": doc.SSHKeys, } } for i, g := range doc.Groups { switch g { case legacyAdminGroup: doc.Groups[i] = auth.AdminUsername case legacyGroupListGroup: doc.Groups[i] = auth.GroupListGroup case legacySSHKeyGetterGroup: doc.Groups[i] = auth.SSHKeyGetterGroup } } return identity, nil } func providerID(doc *mongodoc.Identity) store.ProviderIdentity { if doc.ExternalID == "" { return store.MakeProviderIdentity("idm", doc.Username) } if strings.HasPrefix(doc.ExternalID, "https://login.ubuntu.com/+id") { return store.MakeProviderIdentity("usso", doc.ExternalID) } if strings.HasPrefix(doc.ExternalID, "openid-connect:") { // The only currently used openid provider is azure return store.MakeProviderIdentity("azure", strings.TrimPrefix(doc.ExternalID, "openid-connect:")) } if strings.HasPrefix(doc.ExternalID, "usso-openid:") { return store.MakeProviderIdentity("usso_macaroon", strings.TrimPrefix(doc.ExternalID, "usso-openid:")) } return "" } // Identity implements Source.Identity. func (s *LegacySource) Identity() *store.Identity { return s.identity } // Err implements Source.Err. func (s *LegacySource) Err() error { return errgo.Mask(s.iter.Err()) } golang-github-canonical-candid-1.12.3/cmd/migrate-db/internal/legacy_test.go000066400000000000000000000101201457263123000267130ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package internal_test import ( "context" "testing" "time" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" mgo "github.com/juju/mgo/v2" "github.com/juju/mgotest" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/cmd/migrate-db/internal" "github.com/canonical/candid/cmd/migrate-db/internal/mongodoc" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/store" "github.com/canonical/candid/store/memstore" ) func TestLegacySource(t *testing.T) { c := qt.New(t) defer c.Done() ctx := context.Background() db, err := mgotest.New() if errgo.Cause(err) == mgotest.ErrDisabled { c.Skip("mmgotest disabled") } c.Assert(err, qt.IsNil) defer db.Close() // mgotest sets the SocketTimout to 1s. Restore it back to the // default value. db.Session.SetSocketTimeout(time.Minute) t1 := time.Now().Add(-1 * time.Minute).Round(time.Millisecond) t2 := t1.Add(-1 * time.Minute).Round(time.Millisecond) insert(c, db.Database, &mongodoc.Identity{ Username: "test1", ExternalID: "https://login.ubuntu.com/+id/AAAAAA", Email: "test1@example.com", GravatarID: "f261adc7c891836ecc58c62fb80c6e34", FullName: "Test User", Groups: []string{"group1", "group2"}, SSHKeys: []string{"ssh-rsa AAAAAAAAAAAAAAAAAAAAAAAAAAAA== test@test"}, LastLogin: &t1, LastDischarge: &t2, }) k1 := bakery.MustGenerateKey() insert(c, db.Database, &mongodoc.Identity{ Username: "test2@admin@idm", Owner: "admin@idm", Groups: []string{"admin@idm", "grouplist@idm", "sshkeygetter@idm"}, PublicKeys: []mongodoc.PublicKey{{ Key: k1.Public.Key[:], }}, }) insert(c, db.Database, &mongodoc.Identity{ Username: "test3@azure", ExternalID: "openid-connect:https://login.live.com:AAAAAAAAAAAAAAAAAAAAAIDX0brimGEivOk0995Z2FB", Email: "test3@example.com", FullName: "Test User III", }) insert(c, db.Database, &mongodoc.Identity{ Username: "AAAAAAA@usso", ExternalID: "usso-openid:AAAAAAA", Email: "test4@example.com", FullName: "Test User IV", }) st := memstore.NewStore() err = internal.Copy(ctx, st, internal.NewLegacySource(db.Database)) c.Assert(err, qt.IsNil) identity1 := store.Identity{ Username: "test1", } err = st.Identity(ctx, &identity1) c.Assert(err, qt.IsNil) normalize(&identity1) c.Assert(identity1, qt.DeepEquals, store.Identity{ ProviderID: "usso:https://login.ubuntu.com/+id/AAAAAA", Username: "test1", Email: "test1@example.com", Name: "Test User", Groups: []string{"group1", "group2"}, LastLogin: t1, LastDischarge: t2, ExtraInfo: map[string][]string{ "sshkeys": {"ssh-rsa AAAAAAAAAAAAAAAAAAAAAAAAAAAA== test@test"}, }, }) identity2 := store.Identity{ Username: "test2@admin@idm", } err = st.Identity(ctx, &identity2) c.Assert(err, qt.IsNil) normalize(&identity2) c.Assert(identity2, qt.DeepEquals, store.Identity{ ProviderID: "idm:test2@admin@idm", Username: "test2@admin@idm", Groups: []string{"admin@candid", "grouplist@candid", "sshkeygetter@candid"}, PublicKeys: []bakery.PublicKey{k1.Public}, Owner: auth.AdminProviderID, }) identity3 := store.Identity{ Username: "test3@azure", } err = st.Identity(ctx, &identity3) c.Assert(err, qt.IsNil) normalize(&identity3) c.Assert(identity3, qt.DeepEquals, store.Identity{ ProviderID: "azure:https://login.live.com:AAAAAAAAAAAAAAAAAAAAAIDX0brimGEivOk0995Z2FB", Username: "test3@azure", Name: "Test User III", Email: "test3@example.com", }) identity4 := store.Identity{ Username: "AAAAAAA@usso", } err = st.Identity(ctx, &identity4) c.Assert(err, qt.IsNil) normalize(&identity4) c.Assert(identity4, qt.DeepEquals, store.Identity{ ProviderID: "usso_macaroon:AAAAAAA", Username: "AAAAAAA@usso", Name: "Test User IV", Email: "test4@example.com", }) } func insert(c *qt.C, db *mgo.Database, identity *mongodoc.Identity) { err := db.C("identities").Insert(identity) c.Assert(err, qt.IsNil) } golang-github-canonical-candid-1.12.3/cmd/migrate-db/internal/migrate.go000066400000000000000000000066741457263123000260630ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package internal import ( "context" "log" "strings" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/store" ) // SplitStoreSpecification splits a store specification string as // supplied in the command line arguments into a type and address. func SplitStoreSpecification(s string) (type_, addr string) { n := strings.IndexByte(s, ':') if n < 0 { return s, "" } return s[:n], s[n+1:] } // A Source is the interface that Copy uses to collect migrated // identitites from. type Source interface { // Next fetches the next identity from the source. Next returns // true if the identity was successfully fetched. If there are no // more identities, or an error occurs then false is returned, // the Err method can be used to determine which. Next() bool // Identity returns the current identity for the source. The // pointer is only valid unti Next is called again. Identity() *store.Identity // Err returns any error received whilst getting identities. Err() error } // Copy creates a new identity in dst for every identity retreived from src. func Copy(ctx context.Context, dst store.Store, src Source) error { var failed bool update := store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, store.Groups: store.Set, store.PublicKeys: store.Set, store.LastLogin: store.Set, store.LastDischarge: store.Set, store.ProviderInfo: store.Set, store.ExtraInfo: store.Set, store.Owner: store.Set, } for src.Next() { identity := src.Identity() // The ID field is store specific, so cannot be copied between them. identity.ID = "" destIdentity := store.Identity{ ProviderID: identity.ProviderID, } if err := dst.Identity(ctx, &destIdentity); err != nil { if errgo.Cause(err) != store.ErrNotFound { log.Printf("error checking destination store: %s", err) failed = true continue } } // Only migrate the entry if it is newer than the entry // stored in the destination. This is to make migrations // on running systems safer. if destIdentity.Username == "" || identity.LastLogin.After(destIdentity.LastLogin) { err := dst.UpdateIdentity(ctx, identity, update) if err != nil { log.Printf("cannot update user %s: %s", identity.Username, err) failed = true } } } if failed { return errgo.Newf("some updates failed") } if err := src.Err(); err != nil { return errgo.Notef(err, "cannot read identities") } return nil } // A StoreSource is a Source that wraps a store.Store. type StoreSource struct { index int identities []store.Identity err error } // NewStoreSource creates a new StoreSource that use the given store for // its source of identities. func NewStoreSource(ctx context.Context, st store.Store) *StoreSource { ctx, close := st.Context(ctx) defer close() identities, err := st.FindIdentities(ctx, nil, store.Filter{}, nil, 0, 0) return &StoreSource{ identities: identities, err: err, } } // Next implements Source.Next. func (s *StoreSource) Next() bool { if s.err != nil { return false } s.index++ if s.index > len(s.identities) { return false } return true } // Identity implements Source.Identity. func (s *StoreSource) Identity() *store.Identity { return &s.identities[s.index-1] } // Err implements Source.Err. func (s *StoreSource) Err() error { return s.err } golang-github-canonical-candid-1.12.3/cmd/migrate-db/internal/migrate_test.go000066400000000000000000000167651457263123000271240ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package internal_test import ( "context" "testing" "time" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/cmd/migrate-db/internal" "github.com/canonical/candid/store" "github.com/canonical/candid/store/memstore" ) var splitStoreSpecificationTests = []struct { spec string expectType string expectAddr string }{{ spec: "postgres:host=/var/run/postgresql", expectType: "postgres", expectAddr: "host=/var/run/postgresql", }, { spec: "legacy:", expectType: "legacy", expectAddr: "", }, { spec: "mgo", expectType: "mgo", expectAddr: "", }, { spec: ":something", expectType: "", expectAddr: "something", }, { spec: "", expectType: "", expectAddr: "", }} func TestSplitStoreSpecification(t *testing.T) { c := qt.New(t) defer c.Done() for i, test := range splitStoreSpecificationTests { c.Logf("%d. %s", i, test.spec) type_, addr := internal.SplitStoreSpecification(test.spec) c.Assert(type_, qt.Equals, test.expectType) c.Assert(addr, qt.Equals, test.expectAddr) } } func TestStoreSource(t *testing.T) { c := qt.New(t) defer c.Done() st := memstore.NewStore() ctx := context.Background() err := st.UpdateIdentity(ctx, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "1"), Username: "test1", }, store.Update{ store.Username: store.Set, }) c.Assert(err, qt.IsNil) err = st.UpdateIdentity(ctx, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "2"), Username: "test2", }, store.Update{ store.Username: store.Set, }) c.Assert(err, qt.IsNil) source := internal.NewStoreSource(ctx, st) usernames := make(map[string]struct{}) for source.Next() { usernames[source.Identity().Username] = struct{}{} } c.Assert(source.Err(), qt.Equals, nil) c.Assert(usernames, qt.DeepEquals, map[string]struct{}{ "test1": {}, "test2": {}, }) } func TestStoreSourceEmpty(t *testing.T) { c := qt.New(t) defer c.Done() store := memstore.NewStore() source := internal.NewStoreSource(context.Background(), store) c.Assert(source.Next(), qt.Equals, false) c.Assert(source.Err(), qt.Equals, nil) } func TestStoreSourceError(t *testing.T) { c := qt.New(t) defer c.Done() testError := errgo.New("test error") source := internal.NewStoreSource(context.Background(), errorStore{testError}) c.Assert(source.Next(), qt.Equals, false) c.Assert(source.Err(), qt.Equals, testError) } type errorStore struct { err error } func (s errorStore) Context(ctx context.Context) (context.Context, func()) { return ctx, func() {} } func (s errorStore) Identity(_ context.Context, _ *store.Identity) error { return s.err } func (s errorStore) FindIdentities(_ context.Context, _ *store.Identity, _ store.Filter, _ []store.Sort, _, _ int) ([]store.Identity, error) { return nil, s.err } func (s errorStore) UpdateIdentity(_ context.Context, _ *store.Identity, _ store.Update) error { return s.err } func (s errorStore) IdentityCounts(_ context.Context) (map[string]int, error) { return nil, s.err } func (s errorStore) AddMFACredential(ctx context.Context, cred store.MFACredential) error { return s.err } func (s errorStore) RemoveMFACredential(ctx context.Context, providerID, name string) error { return s.err } func (s errorStore) UserMFACredentials(ctx context.Context, providerID string) ([]store.MFACredential, error) { return nil, s.err } func (s errorStore) IncrementMFACredentialSignCount(ctx context.Context, credentialID []byte) error { return s.err } func (s errorStore) ClearMFACredentials(ctx context.Context, providerID string) error { return s.err } func TestCopy(t *testing.T) { c := qt.New(t) defer c.Done() store1 := memstore.NewStore() ctx := context.Background() k1 := bakery.MustGenerateKey() identity1 := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "1"), Username: "test1", Name: "Test User", Email: "test1@example.com", Groups: []string{"group1", "group2"}, PublicKeys: []bakery.PublicKey{k1.Public}, LastLogin: time.Now().Add(-1 * time.Minute), LastDischarge: time.Now().Add(-2 * time.Minute), ProviderInfo: map[string][]string{ "p1": {"p1v1", "p1v2"}, }, ExtraInfo: map[string][]string{ "e1": {"e1v1", "e1v2"}, }, } err := store1.UpdateIdentity(ctx, &identity1, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, store.Groups: store.Set, store.PublicKeys: store.Set, store.LastLogin: store.Set, store.LastDischarge: store.Set, store.ProviderInfo: store.Set, store.ExtraInfo: store.Set, }) c.Assert(err, qt.IsNil) identity2 := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "2"), Username: "test2", } err = store1.UpdateIdentity(ctx, &identity2, store.Update{ store.Username: store.Set, }) c.Assert(err, qt.IsNil) store2 := memstore.NewStore() err = internal.Copy(ctx, store2, internal.NewStoreSource(ctx, store1)) c.Assert(err, qt.IsNil) copiedIdentity1 := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "1"), } err = store2.Identity(ctx, &copiedIdentity1) c.Assert(err, qt.IsNil) normalize(&identity1) normalize(&copiedIdentity1) c.Assert(copiedIdentity1, qt.DeepEquals, identity1) copiedIdentity2 := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "2"), } err = store2.Identity(ctx, &copiedIdentity2) c.Assert(err, qt.IsNil) normalize(&identity2) normalize(&copiedIdentity2) c.Assert(copiedIdentity2, qt.DeepEquals, identity2) } func normalize(identity *store.Identity) { identity.ID = "" if len(identity.Groups) == 0 { identity.Groups = nil } if len(identity.PublicKeys) == 0 { identity.PublicKeys = nil } if len(identity.ProviderInfo) == 0 { identity.ProviderInfo = nil } if len(identity.ExtraInfo) == 0 { identity.ExtraInfo = nil } } func TestCopySrcError(t *testing.T) { c := qt.New(t) defer c.Done() store1 := errorStore{errgo.New("test error")} ctx := context.Background() store2 := memstore.NewStore() err := internal.Copy(ctx, store2, internal.NewStoreSource(ctx, store1)) c.Assert(err, qt.ErrorMatches, "cannot read identities: test error") } func TestCopyDstError(t *testing.T) { c := qt.New(t) defer c.Done() store1 := memstore.NewStore() ctx := context.Background() k1 := bakery.MustGenerateKey() identity1 := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "1"), Username: "test1", Name: "Test User", Email: "test1@example.com", Groups: []string{"group1", "group2"}, PublicKeys: []bakery.PublicKey{k1.Public}, LastLogin: time.Now().Add(-1 * time.Minute), LastDischarge: time.Now().Add(-2 * time.Minute), ProviderInfo: map[string][]string{ "p1": {"p1v1", "p1v2"}, }, ExtraInfo: map[string][]string{ "e1": {"e1v1", "e1v2"}, }, } err := store1.UpdateIdentity(ctx, &identity1, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, store.Groups: store.Set, store.PublicKeys: store.Set, store.LastLogin: store.Set, store.LastDischarge: store.Set, store.ProviderInfo: store.Set, store.ExtraInfo: store.Set, }) c.Assert(err, qt.IsNil) store2 := errorStore{errgo.New("test error")} err = internal.Copy(ctx, store2, internal.NewStoreSource(ctx, store1)) c.Assert(err, qt.ErrorMatches, "some updates failed") } golang-github-canonical-candid-1.12.3/cmd/migrate-db/internal/mongodoc/000077500000000000000000000000001457263123000256745ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/cmd/migrate-db/internal/mongodoc/doc.go000066400000000000000000000031561457263123000267750ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package mongodoc import ( "time" ) // Identity holds the legacy in-database representation of a user in the // identities mongodb collection. type Identity struct { // Username holds the unique name for the user of the system, which is // associated to the URL accessed through jaas.io/u/username. Username string // ExternalID holds a globally unique name for the user. ExternalID string `bson:"external_id,omitempty"` // Email holds the email address of the user. Email string // GravatarID holds the md5 of email address of the user as a gravatar id. GravatarID string // FullName holds the full name of the user. FullName string `bson:"fullname"` // Owner holds the username of the owner of this user, if there is one. Owner string `bson:",omitempty"` // Groups holds a list of group names to which the user belongs. Groups []string // SSHKeys holds a list of ssh keys owned by the user. SSHKeys []string `bson:"ssh_keys,omitempty"` // PublicKeys contains a list of public keys associated with this account. PublicKeys []PublicKey `bson:"public_keys,omitempty"` // ExtraInfo holds additional information about the user that // is required by other parts of the system. ExtraInfo map[string][]byte `bson:",omitempty" json:",omitempty"` // LastLoginTime holds the time of the last login for this identity. LastLogin *time.Time `bson:",omitempty"` // LastDischargeTime holds the time of the last discharge for this identity. LastDischarge *time.Time `bson:",omitempty"` } type PublicKey struct { Key []byte } golang-github-canonical-candid-1.12.3/cmd/migrate-db/main.go000066400000000000000000000071201457263123000235260ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package main import ( "context" "database/sql" "flag" "fmt" "log" "os" _ "github.com/lib/pq" errgo "gopkg.in/errgo.v1" mgo "github.com/juju/mgo/v2" "github.com/canonical/candid/cmd/migrate-db/internal" "github.com/canonical/candid/store" "github.com/canonical/candid/store/mgostore" "github.com/canonical/candid/store/sqlstore" ) var ( from = flag.String("from", "legacy:mongodb://localhost/identity", "store `specification` to copy the identities from.") to = flag.String("to", "mgo:mongodb://localhost/idm", "store `specification` to copy the identities to.") ) func main() { flag.Usage = usage flag.Parse() if err := migrate(context.Background()); err != nil { log.Println(err) os.Exit(1) } } func usage() { fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) fmt.Fprint(os.Stderr, ` Migrate all of the identities from one store to another. Stores are specified by a string containing the store type, a colon, and connection information specific to the store type. For the -from store the valid prefixes are: "legacy" - old style mgo based store "mgo" - new style mgo based store "postgres" - postgres based store The -to store only supports "mgo" and "postgres". For "legacy" and "mgo" type stores the connection string is a mgo URL (see https://godoc.org/gopkg.in/mgo.v2#Dial). For "postgres" type stores the connection string is as documented in https://godoc.org/github.com/lib/pq. `) flag.PrintDefaults() } func migrate(ctx context.Context) error { var source internal.Source type_, addr := internal.SplitStoreSpecification(*from) switch type_ { case "legacy": s, err := mgo.Dial(addr) if err != nil { return errgo.Notef(err, "cannot connect to mongodb server") } defer s.Close() source = internal.NewLegacySource(s.DB("")) case "mgo": s, err := mgo.Dial(addr) if err != nil { return errgo.Notef(err, "cannot connect to mongodb server") } defer s.Close() backend, err := mgostore.NewBackend(s.DB("")) if err != nil { return errgo.Notef(err, "cannot initialize mgo store") } defer backend.Close() source = internal.NewStoreSource(ctx, backend.Store()) case "postgres": sqldb, err := sql.Open("postgres", addr) if err != nil { return errgo.Notef(err, "cannot connect to postgresql server") } defer sqldb.Close() backend, err := sqlstore.NewBackend("postgres", sqldb) if err != nil { return errgo.Notef(err, "cannot initialize postgresql database") } defer backend.Close() source = internal.NewStoreSource(ctx, backend.Store()) default: return errgo.Newf("invalid source type %q", type_) } var store store.Store type_, addr = internal.SplitStoreSpecification(*to) switch type_ { case "mgo": s, err := mgo.Dial(addr) if err != nil { return errgo.Notef(err, "cannot connect to mongodb server") } defer s.Close() backend, err := mgostore.NewBackend(s.DB("")) if err != nil { return errgo.Notef(err, "cannot initialize mgo store") } defer backend.Close() store = backend.Store() case "postgres": sqldb, err := sql.Open("postgres", addr) if err != nil { return errgo.Notef(err, "cannot connect to postgresql server") } defer sqldb.Close() backend, err := sqlstore.NewBackend("postgres", sqldb) if err != nil { return errgo.Notef(err, "cannot initialize postgresql database") } defer backend.Close() store = backend.Store() default: return errgo.Newf("invalid destination type %q", type_) } ctx, close := store.Context(ctx) defer close() return errgo.Mask(internal.Copy(ctx, store, source)) } golang-github-canonical-candid-1.12.3/config/000077500000000000000000000000001457263123000207425ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/config/config.go000066400000000000000000000164501457263123000225440ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // The config package defines configuration parameters for the id server. package config import ( "crypto/tls" "io/ioutil" "os" "strings" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/loggo" "gopkg.in/errgo.v1" "gopkg.in/yaml.v2" "github.com/canonical/candid/idp" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) var logger = loggo.GetLogger("candid.config") // Config holds the configuration parameters for the identity service. type Config struct { // Storage holds the storage backend to use. Storage *store.Config `yaml:"storage"` // IdentityProviders holds all the configured identity providers. // If this is empty, the default Ubuntu SSO (usso) provider will be used. IdentityProviders []idp.Config `yaml:"identity-providers"` // LoggingConfig holds the loggo configuration to use. LoggingConfig string `yaml:"logging-config"` // ListenAddress holds the address to listen on for HTTP connections to the Candid API // formatted as hostname:port. ListenAddress string `yaml:"listen-address"` // Location holds the external address to use when the API // returns references to itself (for example in third party caveat locations). Location string `yaml:"location"` // AccessLog holds the name of a file to use to write logs of API accesses. AccessLog string `yaml:"access-log"` // RendezvousTimeout holds length of time that an interactive authentication // request can be active before it is forgotten. RendezvousTimeout DurationString `yaml:"rendezvous-timeout"` // PrivateAddr holds the hostname where this instance of the Candid server // can be contacted. This is used by instances of the Candid server // to communicate directly with one another. PrivateAddr string `yaml:"private-addr"` // TLSCert and TLSKey hold a TLS server certificate for the HTTP // server to use. If these are specified, Candid will serve its API // over HTTPS using them. TLSCert string `yaml:"tls-cert"` TLSKey string `yaml:"tls-key"` // PublicKey and PrivateKey holds the key pair used by the Candid // server for encryption and decryption of third party caveats. // These must be specified. // TODO generate these automatically if not specified and store // them in the database. PublicKey *bakery.PublicKey `yaml:"public-key"` PrivateKey *bakery.PrivateKey `yaml:"private-key"` // AdminAgentPublicKey holds the public part of a key pair that // can be used to authenticate as the admin user. If not specified // no public-key-based authentication can be used for the admin // user. AdminAgentPublicKey *bakery.PublicKey `yaml:"admin-agent-public-key"` // AdminPassword holds the password for basic-auth admin // access. If this is empty, no basic-auth authentication will // be allowed. AdminPassword string `yaml:"admin-password"` // ResourcePath holds the path to the directory holding // resources used by the server, including web page templates. ResourcePath string `yaml:"resource-path"` // HTTPProxy holds the address of an HTTP proxy to use for // outgoing HTTP requests, in the same form as the HTTP_PROXY // environment variable. HTTPProxy string `yaml:"http-proxy"` // NoProxy holds which hosts not to use the HTTProxy for, // in the same form as the NO_PROXY environment variable. NoProxy string `yaml:"no-proxy"` // RedirectLoginTrustedURLs contains a list of URLs that are // trusted to be used as return_to URLs during an interactive // login. RedirectLoginTrustedURLs []string `yaml:"redirect-login-trusted-urls"` // RedirectLoginTrustedDomains contains a list of domains that are // trusted to be used as return_to URLs during an interactive // login. RedirectLoginTrustedDomains []string `yaml:"redirect-login-trusted-domains"` // APIMacaroonTimeout is the maximum age an API macaroon can get // before requiring re-authorization. APIMacaroonTimeout DurationString `yaml:"api-macaroon-timeout"` // DischargeMacaroonTimeout is the maximum age a discharge // macaroon can get before it becomes invalid. DischargeMacaroonTimeout DurationString `yaml:"discharge-macaroon-timeout"` // DischargeTokenTimeout is the maximum age a discharge token can // get before it becomes invalid. DischargeTokenTimeout DurationString `yaml:"discharge-token-timeout"` // SkipLocationForCookiePaths instructs if the Cookie Paths are to // be set relative to the Location Path or not. SkipLocationForCookiePaths bool `yaml:"skip-location-for-cookie-paths"` // EnableEmailLogin enables the login with email address link on the // authentication required page. EnableEmailLogin bool `yaml:"enable-email-login"` // MFARPDisplayName holds the relying party display name for MFA. MFARPDisplayName string `yaml:"mfa-rp-display-name"` // MFARPID holds the relying party id for MFA. MFARPID string `yaml:"mfa-rp-id"` // MFARPOrigin holds the relying party origin for MFA. MFARPOrigin string `yaml:"mfa-rp-origin"` // BrandName holds the name of the entity running candid. BrandName string `yaml:"brand-name"` // BrandLogoLocation holds the location of the logo of the entity // running candid. BrandLogoLocation string `yaml:"brand-logo-location"` } // TLSConfig returns a TLS configuration to be used for serving // the API. If the TLS certficate and key are not specified, it returns nil. func (c *Config) TLSConfig() *tls.Config { if c.TLSCert == "" || c.TLSKey == "" { return nil } cert, err := tls.X509KeyPair([]byte(c.TLSCert), []byte(c.TLSKey)) if err != nil { logger.Errorf("cannot create certificate: %s", err) return nil } return &tls.Config{ Certificates: []tls.Certificate{ cert, }, } } func (c *Config) validate() error { var missing []string if c.Storage == nil { // TODO default to in-memory storage? missing = append(missing, "storage") } if c.ListenAddress == "" { missing = append(missing, "listen-address") } if c.PrivateKey == nil { missing = append(missing, "private-key") } if c.PublicKey == nil { missing = append(missing, "public-key") } if c.Location == "" { // TODO check it's a valid URL missing = append(missing, "location") } if c.PrivateAddr == "" { missing = append(missing, "private-addr") } if len(missing) != 0 { return errgo.Newf("missing fields %s in config file", strings.Join(missing, ", ")) } return nil } // Read reads an identity configuration file from the given path. func Read(path string) (*Config, error) { f, err := os.Open(path) if err != nil { return nil, errgo.Notef(err, "cannot open config file") } defer f.Close() data, err := ioutil.ReadAll(f) if err != nil { return nil, errgo.Notef(err, "cannot read %q", path) } var conf Config err = yaml.Unmarshal(data, &conf) if err != nil { return nil, errgo.Notef(err, "cannot parse %q", path) } if err := conf.validate(); err != nil { return nil, errgo.Mask(err) } params.BrandName = conf.BrandName params.BrandLogoLocation = conf.BrandLogoLocation return &conf, nil } // DurationString holds a duration that marshals and unmarshals as a // string in the form printed by time.Duration.String. type DurationString struct { time.Duration } func (dp *DurationString) UnmarshalText(data []byte) error { d, err := time.ParseDuration(string(data)) if err != nil { return errgo.Mask(err) } dp.Duration = d return nil } golang-github-canonical-candid-1.12.3/config/config_test.go000066400000000000000000000204521457263123000236000ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package config_test import ( "io/ioutil" "path" "testing" "time" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/canonical/candid/config" "github.com/canonical/candid/idp" "github.com/canonical/candid/store" _ "github.com/canonical/candid/store/memstore" ) const testConfig = ` listen-address: 1.2.3.4:5678 foo: 1 bar: false admin-password: mypasswd private-key: 8PjzjakvIlh3BVFKe8axinRDutF6EDIfjtuf4+JaNow= public-key: CIdWcEUN+0OZnKW9KwruRQnQDY/qqzVdD30CijwiWCk= admin-agent-public-key: dUnC8p9p3nygtE2h92a47Ooq0rXg0fVSm3YBWou5/UQ= location: http://foo.com:1234 storage: type: test attribute: hello rendezvous-timeout: 1m identity-providers: - type: usso - type: keystone name: ks1 url: http://example.com/keystone private-addr: localhost tls-cert: | -----BEGIN CERTIFICATE----- MIIDLDCCAhQCCQDVXrWn1thP6DANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJH QjENMAsGA1UECAwEVGVzdDENMAsGA1UEBwwEVGVzdDENMAsGA1UECgwEVGVzdDEN MAsGA1UECwwEVGVzdDENMAsGA1UEAwwEVGVzdDAeFw0xNjA3MDcxMjE2MDBaFw0z NjA3MDIxMjE2MDBaMFgxCzAJBgNVBAYTAkdCMQ0wCwYDVQQIDARUZXN0MQ0wCwYD VQQHDARUZXN0MQ0wCwYDVQQKDARUZXN0MQ0wCwYDVQQLDARUZXN0MQ0wCwYDVQQD DARUZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3uRyzTaYMWj/ aGjqQtCMf4VMLIcR4o+yJVUp7CvhHIa/Ykx32OZMLth6DihykYzOFZj9wzD2a+GB 8P3RkDMP5dxQF9yQSTTl/Ec7ZkHHnJzpao9mGsfJ7h24F4XTKC7QovaNw5HV83ej Vwrose8BHe5UlEpncTIqOY3JJbzzkrzSMzS7cGB1l55zXpDQVcRzv/182qFX2L3+ ukIlbt3PNAjGPgKWYeVameTL38oKjJ5ftrADWjAWc7IBPw65KvqOTj5Jw+Jhkj4H 4kkXKKn8N6ItiWclpWuKi8Va36VVUXnqPxOWnIK4AGnO8WEArRhU7XK+EiFK8TuH SSrOh9myWQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBvuGuwGrMSHNOKrWrWnwKD T3Ge9FfUonBmkzvGmWHfLROju3mwxAP0lB10+sn1gnjUHzKiVjeY8fuAjFQMrKUp HUWCaVPjsExd2OYu+6f+06rTrP98BNopuYWeIIkmc3JoFwOmSKTA5JIlNBDsN5F/ PFcXE9Xjc4Ob1ut/bv6hJ1nbgbaVSNB4Zc+3oxi2X+xBut8zqATq7JYvO0SVH6h1 oSp3lveosF9AQB8uLtWZFf3wnburr2zG6UhkSwQdy0GYEwTtqaYs7Ue7bHvO0GYG zPCVixoo4QoTiwDV7HGodrjvcMgtUgoDOhR6daZPEYV6rQJJoGhMF5+UAgS2KiMh -----END CERTIFICATE----- tls-key: | -----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA3uRyzTaYMWj/aGjqQtCMf4VMLIcR4o+yJVUp7CvhHIa/Ykx3 2OZMLth6DihykYzOFZj9wzD2a+GB8P3RkDMP5dxQF9yQSTTl/Ec7ZkHHnJzpao9m GsfJ7h24F4XTKC7QovaNw5HV83ejVwrose8BHe5UlEpncTIqOY3JJbzzkrzSMzS7 cGB1l55zXpDQVcRzv/182qFX2L3+ukIlbt3PNAjGPgKWYeVameTL38oKjJ5ftrAD WjAWc7IBPw65KvqOTj5Jw+Jhkj4H4kkXKKn8N6ItiWclpWuKi8Va36VVUXnqPxOW nIK4AGnO8WEArRhU7XK+EiFK8TuHSSrOh9myWQIDAQABAoIBAGP7qhuvv7l6Vgep +FucXUneq3rV5AnzV4AzoaiVTleTgko/7wrW05m39ZhgQHRV6yP5CuwCDKf78mP+ F4FNxnXfy/XINNkB56Cw+041d6sjH/ly9eRRdp1fq3KxzzSZO3G+k30E8CpUomqr NBKNGb0pabtTXO+EBzjmBzLsfX52anGEi2U2I/Q2srU+3FAkhjb9s9ZSgWh9zgrS 0sK/oO04dlTLV0weq2oTHCX/ygQZpXvRXNJJVDRtst3R9EfUKr4YLWEK2k1PgWC+ 52CJoYETbQPGiJbzReTgYTlZYHSZfuso20sPfOc01qgcJIk5qOAS2dgU5EanSQEM 0/HJ02ECgYEA+lHafm4psqi6YWLV0Evr54kzUVYXaBY/8Qbf4psCZ0o9VjfwzIPG ncgGXhyv9qlnFx38YEKAvn/HV52J8Qi5I8k4TBtYB/GYcNvpcNgR6uMcg+nS+0nf Y0BJgyUwY7Exh2BTIkJKLzIoOK0RKe1pk99Iboee9MDv6YaHQqlXaGUCgYEA4/ND 3jb0PTEDrCtDTYOhNqcW/ER6rq8vSwR7uiHGBY6OiYcFgmV/AC3SUpVQurw5YIxh kQ1s7ncdBNN6fOpUEFYmBhPAkoHbVIcg98ZnzqM3tQU9o4sujT9pd8ATthAlqaBR G+5s7Cil9RtggCBXL1G+CQPS2TJoE8Tr/SfnEOUCgYA4Dx7Ek71I4pqi9rR1qpsR Rlu0yngBeoIlY2m+YQKfyTOFXI/T7WsMqOAsMXaC4htRRQjhMeONRiaJi6F51n9H 8WdnO/RyCvwdwlI8UFdq6CPZswLp/fhGTP5pnWmB2gwCimLz2C6u9Sem0bN3VVEA qc+Z2UuS+qaAAP3Hww7tNQKBgQDO6gXEEzwWw4Qi5057cS2Ib5m0ufBm2oxiWxp4 danLZ4DJI7ADkl/66J0O64zRRIQMuMDjqz0jJSpJNDHua8KM5bY0M//MvWU7UEHD x+x4rL2naq9t4awK+PGiis8Zp4SYefbGFOH4aFlkqUoqY7DgOiH3Cup8z32b3Fee f3cGZQKBgQDvsz2cBGNFW+U03sDeHqBbdim6E2RRvPrxLkeljSiU9RzJ3P76Ousv ORfedwfVln37uivduCeyBLMhaYWiW6CN4Di/d8LsI1hwe1MlNHuV2EptaFDzfjx8 FWQQKAkL5KolhJye0Kz/X8CT3UMmhOK73UkUaOvMvdSjxLFgIruxWQ== -----END RSA PRIVATE KEY----- resource-path: /resources http-proxy: http://proxy.example.com:3128 no-proxy: localhost,.example.com redirect-login-trusted-urls: - https://example.com/1 - https://example.com/2 redirect-login-trusted-domains: - www.example.com - "*.example.net" api-macaroon-timeout: 2h discharge-macaroon-timeout: 24h discharge-token-timeout: 6h enable-email-login: true ` func readConfig(c *qt.C, content string) (*config.Config, error) { // Write the configuration content to file. path := path.Join(c.Mkdir(), "config.yaml") err := ioutil.WriteFile(path, []byte(content), 0666) c.Assert(err, qt.IsNil) // Read the configuration. return config.Read(path) } func TestRead(t *testing.T) { c := qt.New(t) defer c.Done() idp.Register("usso", testIdentityProvider) idp.Register("keystone", testIdentityProvider) store.Register("test", testStorageBackend) conf, err := readConfig(c, testConfig) c.Assert(err, qt.IsNil) // Check that the TLS configuration creates a valid *tls.Config tlsConfig := conf.TLSConfig() c.Assert(tlsConfig, qt.Not(qt.IsNil)) conf.TLSCert = "" conf.TLSKey = "" var key bakery.KeyPair err = key.Public.UnmarshalText([]byte("CIdWcEUN+0OZnKW9KwruRQnQDY/qqzVdD30CijwiWCk=")) c.Assert(err, qt.IsNil) err = key.Private.UnmarshalText([]byte("8PjzjakvIlh3BVFKe8axinRDutF6EDIfjtuf4+JaNow=")) c.Assert(err, qt.IsNil) var adminPubKey bakery.PublicKey err = adminPubKey.UnmarshalText([]byte("dUnC8p9p3nygtE2h92a47Ooq0rXg0fVSm3YBWou5/UQ=")) c.Assert(err, qt.IsNil) c.Assert(conf, qt.DeepEquals, &config.Config{ Storage: &store.Config{ BackendFactory: storageBackend{ Params: map[string]string{ "type": "test", "attribute": "hello", }, }, }, IdentityProviders: []idp.Config{{ IdentityProvider: identityProvider{ Params: map[string]string{ "type": "usso", }, }, }, { IdentityProvider: identityProvider{ Params: map[string]string{ "type": "keystone", "name": "ks1", "url": "http://example.com/keystone", }, }, }}, ListenAddress: "1.2.3.4:5678", AdminPassword: "mypasswd", PrivateKey: &key.Private, PublicKey: &key.Public, AdminAgentPublicKey: &adminPubKey, Location: "http://foo.com:1234", RendezvousTimeout: config.DurationString{Duration: time.Minute}, PrivateAddr: "localhost", ResourcePath: "/resources", HTTPProxy: "http://proxy.example.com:3128", NoProxy: "localhost,.example.com", RedirectLoginTrustedURLs: []string{ "https://example.com/1", "https://example.com/2", }, RedirectLoginTrustedDomains: []string{ "www.example.com", "*.example.net", }, APIMacaroonTimeout: config.DurationString{Duration: 2 * time.Hour}, DischargeMacaroonTimeout: config.DurationString{Duration: 24 * time.Hour}, DischargeTokenTimeout: config.DurationString{Duration: 6 * time.Hour}, EnableEmailLogin: true, }) } func TestReadErrorNotFound(t *testing.T) { c := qt.New(t) defer c.Done() cfg, err := config.Read(path.Join(c.Mkdir(), "no-such-file.yaml")) c.Assert(err, qt.ErrorMatches, ".* no such file or directory") c.Assert(cfg, qt.IsNil) } func TestReadErrorEmpty(t *testing.T) { c := qt.New(t) defer c.Done() cfg, err := readConfig(c, "") c.Assert(err, qt.ErrorMatches, "missing fields storage, listen-address, private-key, public-key, location, private-addr in config file") c.Assert(cfg, qt.IsNil) } func TestReadErrorInvalidYAML(t *testing.T) { c := qt.New(t) defer c.Done() cfg, err := readConfig(c, ":") c.Assert(err, qt.ErrorMatches, "cannot parse .*: yaml: did not find expected key") c.Assert(cfg, qt.IsNil) } func TestUnrecognisedIDP(t *testing.T) { c := qt.New(t) defer c.Done() cfg, err := readConfig(c, ` identity-providers: - type: nosuch `) c.Assert(err, qt.ErrorMatches, `cannot parse ".*": unrecognised identity provider type "nosuch"`) c.Assert(cfg, qt.IsNil) } type identityProvider struct { idp.IdentityProvider Params map[string]string } func testIdentityProvider(unmarshal func(interface{}) error) (idp.IdentityProvider, error) { idp := identityProvider{ Params: make(map[string]string), } if err := unmarshal(&idp.Params); err != nil { return nil, err } return idp, nil } type storageBackend struct { store.BackendFactory Params map[string]string } func testStorageBackend(unmarshal func(interface{}) error) (store.BackendFactory, error) { backend := storageBackend{ Params: make(map[string]string), } if err := unmarshal(&backend.Params); err != nil { return nil, err } return backend, nil } golang-github-canonical-candid-1.12.3/docs/000077500000000000000000000000001457263123000204255ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/docs/configuration.md000066400000000000000000000612561457263123000236300ustar00rootroot00000000000000Candid Configuration ============================== Introduction ------------ This document describes the configuration options for the Candid identity server. Configuration File ------------------ Candid loads its configuration at startup from a YAML file. In a usual installation this file is stored in /etc/candid/config.yaml. An example configuration file is: ```yaml listen-address: :8081 location: 'http://jujucharms.com/identity' storage: type: mongodb address: localhost:27017 public-key: OAG9EVDFgXzWQKIk+MTxpLVO1Mp1Ws/pIkzhxv5Jk1M= private-key: q2G3A2NjTe7MP9D8iugCH9XfBAyrnV8n8u8ACbNyNOY= identity-providers: - type: usso ``` Here is a description of the most commonly used configuration options. Some less useful options are omitted here - the remaining ones are all documented [here](https://godoc.org/github.com/CanonicalLtd/candid/config#Config). ### listen-address (Required) This is the address that the service will listen on. This consists of an optional host followed by a port. If the host is omitted then the server will listen on all interface addresses. The port may be a well known service name for example ":http". ### location (Required) This is the externally addressable location of the Candid server API. Candid needs to know its own address so that it can add third-party caveats addressed to itself and to create response addresses for identity providers such as OpenID that use browser redirection for communication. ### storage Storage holds configuration for the storage backend used by the server. See below for documentation on the supported storage backends. ### public-key & private-key (Required) Services wishing to discharge caveats against this identity manager encrypt their third party caveats using this public-key. The private key is needed for the identity manager to be able to discharge those caveats. You can use the `bakery-keygen` command (available with `go install gopkg.in/macaroon-bakery.v2/cmd/bakery-keygen` to generate a suitable key pair. ### access-log The access-log configures the name of a file used to record all accesses to the identity manager. If this is not configured then no logging will take place. ### identity-providers This is a list of the configured identity providers with their configuration. See below for the supported identity providers. If this is not configured then a default set of providers will be used containing the Ubuntu SSO and Agent identity providers. ### api-macaroon-timeout This is the maximum time a login to the /v1 API will remain logged in for. As candid uses itself as its authentication provider, for all practical purpose the login time will be the minimum of `api-macaroon-timeout` and `discharge-macaroon-timeout` . The default value is 24 hours. ### discharge-macaroon-timeout This is the maximum time the discharge macaroon will be valid for on the target service. This is the maximum time the client will be able to access the target service without requiring re-authentication. Note that the target service may also have its own maximum time. ### discharge-token-timeout This is the maximum time that the discharge token issued to the client can be used to discharge tokens without requiring re-authentication. ### redirect-login-trusted-urls This is a list of trusted return-to addresses for use in the redirect-based login process. If a redirect-based login is attempted with a return-to address that does not match an entry in either `redirect-login-trusted-urls` or `redirect-login-trusted-domains` then candid will show an error page rather than redirect the user's browser. To match an entry in redirect-login-trusted-urls the return-to address must match exactly. ### redirect-login-trusted-domains This is a list of trusted domains that are used in return-to addresses for use in the redirect-based login process. If a redirect-based login is attempted with a return-to address that does not match an entry in either `redirect-login-trusted-domains` or `redirect-login-trusted-urls` then candid will show an error page rather than redirect the user's browser. Entries in the the `redirect-login-trusted-domains` list take the form of either a full host name (e.g `www.example.com`) or a wildcard domain (e.g. `*.example.com`). The former type causes all return-to URLs with a host part that exactly matches the entry to be trusted. The latter type causes all return-to URLs with a host part that is a subdomain of the specified domain to be trusted. Please note that all paths in a `redirect-login-trusted-domain` are trusted, so these should only be used where a trusted party controls the entire domain. ### mfa-rp-display-name This is the name of the candid as a relying party for the multi-factor authentication. ### mfa-rp-id This is the id of candid as a relying party for the multi-factor authentication - in general this should be set to the FQDN of candid. ### mfa-rp-origin This is the origin url of the WebAuthn requests for candid. Storage Backends ----------- The `storage` field holds an object containing a `type` field which names the storage backend to use. For example: storage: type: mongodb address: localhost:1234 Currently supported backends are: ### memory The memory provider has no extra parameters. It stores all data ephemerally in RAM. ### mongodb This uses MongoDB for the backend. It has two parameters: `address` (required) is the address of the mongoDB server to connect to, in `host:port` form. `database` holds the database name to use. If not specified, this will default to `candid` . ### postgres This uses PostgresQL for the backend. It takes one parameter: `connection-string` is the connection string to use when connecting to the database. This is added to connection string parameters already present as environment variables when making a connection. See [here](https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters) for details. Identity Providers ------------------ The identity manager can support a number of different identity providers. These can be broken loosely into two categories, interactive and custom. Interactive providers use html based forms in some way to authorize the user and are compatible with the most basic supported clients. Custom providers use a protocol not necessarily supported in the client to provide additional authentication methods that are not necessarily based around users interacting with web pages. While it is possible to configure more than one interactive identity provider in a given identity manager, in most case this does not make sense as the identity manager will only use the first one that is found. ### Agent The agent identity provider is a custom provider that is always configured, and allows non-interactive logins to clients using public-key authentication. the agent protocol to log in. See https://github.com/canonical/candid/blob/master/docs/login.txt for details on the agent login protocol. ### UbuntuSSO ```yaml - type: usso name: usso domain: external icon: /static/images/usso-icon.bmp description: Ubuntu SSO launchpad-teams: - group1 - group2 staging: false fixed-username: false ``` The UbuntuSSO identity provider is an interactive identity provider that uses OpenID with UbuntuSSO to log in. The `name` parameter specifies the name of the provider, this should be a short name that reflects the name of the system being logged in to. The name is used in some URLS and is best if it consists only of lower-case letters. The `domain` is a string added to the names of users logging in through this identity provider. The user jsmith for example would be changed to jsmith@example in the configuration above. If no domain is specified the username will remain unchanged. The `description` is optional and will be used if the identity provider is presented in a human readable form, if this is not set "Ubuntu SSO" will be used. The `icon` is optional and specifies the location of an icon to display when presenting the identity-provider options to a user. It this is set to URL path then that path should be relative to the candid service's location. If this is not set a default icon for Ubuntu SSO will be used. The `launchpad-teams` contains any private launchpad teams that candid needs to know about. If `staging` is true then the identity provider will use staging instances of Ubuntu SSO and launchpad for the identity information. If `fixed-username` is true then username changes returned from Ubuntu SSO will not be automatically reflected when a user authenticates. The username in candid will remain fixed to the username that is first used. ### UbuntuSSO OAuth ```yaml - type: usso_oauth ``` The UbuntuSSO OAuth identity provider is an custom identity provider that uses a previously obtained UbuntuSSO OAuth token to log in. ### Keystone ```yaml - type: keystone name: canonistack domain: canonistack description: Canonistack icon: /static/images/keystone-icon.bmp url: https://keystone.canonistack.canonical.com:443/ hidden: false ``` The Keystone identity provider is an interactive identity provider that uses a keystone service to log the user in using their openstack credentials. The Keystone identity provider has a number of additional options. The `name` parameter specifies the name of the provider, this should be a short name that reflects the name of the system being logged in to. The name is used in some URLS and is best if it consists only of lower-case letters. The `domain` is a string added to the names of users logging in through this identity provider. The user jsmith for example would be changed to jsmith@canonistack in the configuration above. If no domain is specified the username will remain unchanged. The `description` is optional and will be used if the identity provider is presented in a human readable form, if this is not set the name will be used. The `icon` is optional and specifies the location of an icon to display when presenting the identity-provider options to a user. It this is set to URL path then that path should be relative to the candid service's location. If this is not set a default icon for keystone will be used. The `url` is the location of the keystone server that will be used to authenticate the user. The `hidden` value is an optional value that can be used to not list this identity provider in the list of possible identity providers when performing an interactive login. ### Keystone Token ```yaml - type: keystone_token name: jujugui domain: canonistack description: Canonistack url: https://keystone.canonistack.canonical.com:443/ ``` The Keystone Token identity provider is a custom identity provider that uses a keystone service to authenticate a user that already has a keystone authentication token by logging in previously through some external means. It is designed to be used in jujugui system embedded in horizon services to prevent a user having to log in twice. The Keystone Token identity provider has a number of additional options. The `name` parameter specifies the name of the provider. The name is used in some URLS and is best if it consists only of lower-case letters. The name "jujugui" can be used to indicate to a jujugui instance that this provider can be used to log in with an existing token. The `domain` is a string added to the names of users logging in through this identity provider. The user jsmith for example would be changed to jsmith@canonistack in the configuration above. If no domain is specified the username will remain unchanged. The `description` is optional and will be used if the identity provider is presented in a human readable form, if this is not set the name will be used. The `url` is the location of the keystone server that will be used to authenticate the user. ### Keystone Userpass ```yaml - type: keystone_userpass name: form domain: canonistack description: Canonistack url: https://keystone.canonistack.canonical.com:443/ ``` The Keystone Userpass identity provider is a custom identity provider that uses a keystone service to authenticate users that have provided their username and password through a form mechanism in the client. It is designed to allow credentials to be provided through a CLI where web page access is not practical. The Keystone Userpass identity provider has a number of additional options. The `name` parameter specifies the name of the provider. The name is used in some URLS and is best if it consists only of lower-case letters. The name "form" can be used to indicate to clients that support the form protocol that the protocol can be used. The `domain` is a string added to the names of users logging in through this identity provider. The user jsmith for example would be changed to jsmith@canonistack in the configuration above. If no domain is specified the username will remain unchanged. The `description` is optional and will be used if the identity provider is presented in a human readable form, if this is not set the name will be used. The `url` is the location of the keystone server that will be used to authenticate the user. ### Azure OpenID Connect ```yaml - type: azure icon: /static/images/azure-icon.bmp client-id: 43444f68-3666-4f95-bd34-6fc24b108019 client-secret: tXV2SRFflAGT9sUdxkdIi7mwfmQ= hidden: false ``` The Azure identity provider uses OpenID Connect to log in using Microsoft credentials via https://login.live.com. When a user first logs in with this IDP they will be prompted to create a new identity. The new identity must have a unique username and will be in the domain "@azure". The `icon` is optional and specifies the location of an icon to display when presenting the identity-provider options to a user. It this is set to URL path then that path should be relative to the candid service's location. If this is not set a default icon for azure will be used. The `client-id` and `client-secret` parameters must be specified and are created by registering the candid instance as an application at https://apps.dev.microsoft.com. When registering the application the redirect URLs should include `$CANDID_URL/login/azure/callback` . The `hidden` value is an optional value that can be used to not list this identity provider in the list of possible identity providers when performing an interactive login. ### ADFS OpenID Connect ```yaml - type: adfs name: example domain: example icon: /static/images/adfs.bmp url: https://adfs.example.com client-id: 43444f68-3666-4f95-bd34-6fc24b108019 client-secret: tXV2SRFflAGT9sUdxkdIi7mwfmQ= hidden: true match-email-addr: @example.com$ ``` The ADFS identity provider uses OpenID Connect to authenticate with an Active Directory Federation Services deployment. The `icon` is optional and specifies the location of an icon to display when presenting the identity-provider options to a user. It this is set to URL path then that path should be relative to the candid service's location. If this is not set a default generic OpenID icon will be used. The required `url` parameter specifies the location of the ADFS OpenID Connect service. OpenID Connect Discovery will be performed using this URL to determine the correct endpoints, keys and other parameters required to successfully perform OpenID Connect authentication. The `client-id` and `client-secret` parameters must be specified and are created by registering the candid instance as an application on the ADFS service. When registering the application the redirect URLs should include `$CANDID_URL/login/{name}/callback` . When authenticating candid requests the "email" and "profile" scopes in addition to the "openid" scope in order to retrieve the required profile information. The `hidden` value is an optional value that can be used to not list this identity provider in the list of possible identity providers when performing an interactive login. The `match-email-addr` value is a regular expression that can be used to select the identity provider using an email address. If configured when a user attempts to login via an email address the address will be checked against the regular expression and if they match the identity provider will be used to perform the login. ### Google OpenID Connect ```yaml - type: google icon: /static/images/google-icon.bmp client-id: 483156874216-rh0j89ltslhuqirk7deh70d3mp49kdvq.apps.googleusercontent.com client-secret: 8aENrwCL/+PU87ROkXwMB+09xe0= hidden: false ``` The Google identity provider uses OpenID Connect to log in using Google credentials. When a user first logs in with this IDP they will be prompted to create a new identity. The new identity must have a unique username and will be in the domain "@google". The `client-id` and `client-secret` parameters must be specified and are created by registering the candid instance as an application at https://console.developers.google.com/apis/credentials. When registering the application the authorized redirect URLs should include `$CANDID_URL/login/google/callback` . The `icon` is optional and specifies the location of an icon to display when presenting the identity-provider options to a user. It this is set to URL path then that path should be relative to the candid service's location. If this is not set a default icon for google will be used. The `hidden` value is an optional value that can be used to not list this identity provider in the list of possible identity providers when performing an interactive login. ### Keycloak OpenID Connect ```yaml - type: keycloak domain: example client-id: 483156874216 client-secret: 32hf3uhud23dS@#e keycloak-realm: https://example.com/auth/realms/example hidden: false ``` The Keycloak identity provider uses OpenID Connect to log in using configured credentials. When a user first logs in with this IDP they will be prompted to create a new identity. The new identity must have a unique username and will be in the domain specified "@domain", otherwise default to "@KEYCLOAK". The `icon` is optional and specifies the location of an icon to display when presenting the identity-provider options to a user. It this is set to URL path then that path should be relative to the candid service's location. If this is not set a default generic OpenID icon will be used. The 'keycloak-realm and `client-id` parameters must be specified and should be provided by the keycloak service administrator. An optional client-secret may also be required which the keycloak service administrator should provide. When registering the application the authorized redirect URLs should include `$CANDID_URL/login/keycloak/callback` . The `hidden` value is an optional value that can be used to not list this identity provider in the list of possible identity providers when performing an interactive login. ### LDAP ```yaml - type: ldap name: ldap description: LDAP Login icon: /static/images/ldap-icon.bmp domain: example url: ldap://ldap.example.com/dc=example,dc=com ca-cert: | -----BEGIN CERTIFICATE----- MIIBWTCCAQOgAwIBAgIBADANBgkqhkiG9w0BAQsFADAbMRkwFwYDVQQDExBsZGFw LmV4YW1wbGUuY29tMB4XDTE4MDQxODEwMDUzMVoXDTI4MDQyMDEwMDUzMVowGzEZ MBcGA1UEAxMQbGRhcC5leGFtcGxlLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgC QQDN2tltcVwW0bs80ABocjSZrqBDnpuxnzq2DlrLL+hldwDxVZ0sqU+o768GB6bP 8k3WVf81yYBRbfq7pD/MX0BhAgMBAAGjMjAwMA8GA1UdEwEB/wQFMAMBAf8wHQYD VR0OBBYEFEMAeAXsITzTXHDfJSzrezBkaSvwMA0GCSqGSIb3DQEBCwUAA0EAw6Rh RlR4L5mvvDaN4NP/aNOaWGe+x1Oa7V3L75MmD3DbwcUgDCn45EaUGofbOTrbYuzm mrVoMF002dpQoqc38w== -----END CERTIFICATE----- dn: cn=candid,dc=example,dc=com password: 6IaWWtW/aTN0CIVYwLgeOayyZW8o user-query-filter: (objectClass=account) user-query-attrs: id: uid email: mail display-name: displayName group-query-filter: (&(objectClass=groupOfNames)(member={{.User}})) hidden: false require-mfa: true ``` The LDAP identity provider allows a user to login using an LDAP server. Candid will prompt for a username and password and attempt to use those to authenticate with the LDAP server. `name` is the name to use for the LDAP IDP instance. It is possible to configure more than one LDAP IDP on a given candid server and this allows them to be identified. The name will be used in the login URL. `description` (optional) provides a human readable description of the identity provider. If it is not set it will default to the value of `name` . `icon` (optional) specifies the location of an icon to display when presenting the identity-provider options to a user. It this is set to URL path then that path should be relative to the candid service's location. If this is not set a default generic LDAP icon will be used. `domain` (optional) is the domain in which all identities will be created. If this is not set then no domain is used. `url` contains the URL of the LDAP server being authenticated against. The path component of the URL is used as the base DN for the connection. `ca-cert` (optional) contains the CA certificate that signed the LDAPs server certificate. If this is not set then the connection either has to be unauthenticated or the CA certificate has to be in the system's certificate pool. `dn` (optional) contains the distinguished name that candid uses to bind to the LDAP server to perform searches. If this is not configured then candid binds anonymously and `password` is ignored. `password` (optional) contains the password used when candid binds to the LDAP server. `user-query-filter` contains the filter that candid uses when attempting to find the user that is authenticating. `user-query-attrs` contains the attributes candid uses when searching for authenticating users. When authenticating a user candid will perform a search like `($id=$username)` where the value of `$id` is specified in the `id` parameter and $username is the value entered by the authenticating user. `email` and `display-name` are used to populate the created identity. `group-query-filter` contains the filter candid uses when finding group memberships for a user. The filter is specified as a template (see https://golang.org/pkg/text/template) where the value of `. User` will be replaced with the DN of the user for whom candid is attempting to find group memberships. The `hidden` value is an optional value that can be used to not list this identity provider in the list of possible identity providers when performing an interactive login. If `require-mfa` is set to `true` candid will require users to present valid MFA credentials when logging in. ### Static Identity Provider ```yaml - type: static name: static domain: mydomain description: Static Identity Provider icon: /static/images/static-icon.bmp users: user1: name: User One email: user1@example.com password: password1 groups: [group1, group2] user2: name: User Two email: user2@example.com password: password2 groups: [group3, group4] hidden: false match-email-addr: @example.com$ require-mfa: true ``` The `static` identity provider is meant for testing and allows defining a set of users that can authenticate, along with their passwords and a list of groups they are part of. Note that this provider is *not meant for production use* as it's insecure. `name` is the name to use for the LDAP IDP instance. It is possible to configure more than one LDAP IDP on a given candid server and this allows them to be identified. The name will be used in the login URL. `domain` (optional) is the domain in which all identities will be created. If this is not set then no domain is used. `description` (optional) provides a human readable description of the identity provider. If it is not set it will default to the value of `name` . `icon` (optional) specifies the location of an icon to display when presenting the identity-provider options to a user. It this is set to URL path then that path should be relative to the candid service's location. If this is not set a default icon will be used. `users` contains a static mapping of username to user entries for all of the users defined by the identity provider. The `hidden` value is an optional value that can be used to not list this identity provider in the list of possible identity providers when performing an interactive login. The `match-email-addr` value is a regular expression that can be used to select the identity provider using an email address. If configured when a user attempts to login via an email address the address will be checked against the regular expression and if they match the identity provider will be used to perform the login. If `require-mfa` is set to `true` candid will require users to present valid MFA credentials when loggin in. Charm Configuration ------------------- If the candid charm is being used then most of the parameters will be set with sensible defaults. The charm parameters that must be configured for each deployment are: * password * private-key * public-key * location Most deployments will probably also want to configure the identity-providers unless the default ones are being used. golang-github-canonical-candid-1.12.3/docs/login.txt000066400000000000000000000142221457263123000222770ustar00rootroot00000000000000 Login Methods _TODO_ This document is out of date and should be updated to reflect all the available login methods. 1. Introduction IdM supports these following possible login methods: * Interactive * Agent * UbuntuSSO OAuth Interactive is used by default and does not require any special handling by the client software, it does require interaction with the user so is not suitable for all situations. All of these login methods are initiated as part of the macaroon handling and need to be handled in the VisitWebPage function provided to httpbakery.Client. 2. Interactive Login Interactive login is the default. For the client to use interactive login it needs to arrange for the user to visit the provided web address. Interactive login currently uses UbuntuSSO OpenID login. It is anticipated this will be expanded in the future. 3. Agent Login Agents are users in the system that represents services rather than people. Agents must have been pre-registered with the identity manager. 3.1 Login Method Discovery When a client makes a discharge request and Candid doesn't know about the client, it will return an *interaction-required* error containing information about the available login methods. For example: { "Code": "interaction required", "Message": "interaction required", "Info": { "InteractionMethods": { "agent": { "login-url": "https://candid-address/login/agent" }, "browser-window": { "VisitURL": "https://candid-address/login", "WaitURL": "https://candid-address/wait" } } } } For agent login use the `login-url` field. Each entry in the `InteractionMethods` object specifies a different protocol to follow. All the protocols result in the client gaining a *discharge token*. The client can make the original discharge again with that discharge token to obtain the discharge macaroon. 3.2 Agent Login Request To perform the agent login the client POSTs a JSON object like the following to the specified agent URL: { "username": "...", "public_key": "...", } username contains the username of the identity agent that is logging in. public_key contains a base64 encoding of the public key that will be used to authenticate the agent. When identity discharges a third-party caveat for the agent, an additional third-party caveat is added forcing the agent to prove it has the private key associated with the public key. This part is handled by httpbakery. 3.3 Code Example func AgentDo(req *http.Request, username string, key *bakery.KeyPair) (*http.Response, error) { client = httpbakery.NewClient() client.Key = key client.VisitWebPage = func(u *url.URL) error { req, err := http.NewRequest("GET", u.String(), nil) if err != nil { return err } resp, err := client.Do(req) if err != nil { return err } defer resp.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } var methods params.LoginMethods if err := json.Unmarshal(body, &methods); err != nil { return err } if methods.Agent == "" { return errors.New("agent login not supported") } body, err := json.Marshal(params.AgentLoginRequest{ Username: username, PublicKey: key.Public, }) if err != nil { return err } req, err = http.NewRequest("POST", methods.Agent, nil) if err != nil { return err } resp, err = client.DoWithBody(req, bytes.NewReader(body)) if err != nil { return err } defer resp.Close() if resp.StatusCode != http.StatusOK { return errors.New("login failed") } return nil } return client.Do(req) } 4. UbuntuSSO OAuth Login UbuntuSSO OAuth login provides a non-interactive method for user logins. To use the oauth mechanism the client must have obtained an oauth token from UbuntuSSO to use with this mechanism. For the login to work the user must have previously logged in using interactive login at least once. 4.1. Login Method Discovery Any client that wishes to use UbuntuSSO Oauth login must perform login method discovery (see section 3.1). The URL to use is "usso_oauth". 4.2. UbuntuSSO Login Request A client should then send a GET request to the "usso_oauth" URL that has been signed using the oauth token as specified in RFC5849 [http://tools.ietf.org/html/rfc5849]. 4.3. Code Example func OAuthDo(req *http.Request, tok oauth.Token) (*http.Response, error) { client = httpbakery.NewClient() client.VisitWebPage = func(u *url.URL) error { req, err := http.NewRequest("GET", u.String(), nil) if err != nil { return err } resp, err := client.Do(req) if err != nil { return err } defer resp.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } var methods params.LoginMethods if err := json.Unmarshal(body, &methods); err != nil { return err } if methods.UbuntuSSOOAuth == "" { return errors.New("UbuntuSSO OAuth login not supported") } req, err = http.NewRequest("GET", methods.UbuntuSSOOAuth, nil) if err != nil { return err } oauth.Sign(req, tok) resp, err = client.Do(req, bytes.NewReader(body)) if err != nil { return err } defer resp.Close() if resp.StatusCode != http.StatusOK { return errors.New("login failed") } return nil } return client.Do(req) } Note: The oauth handling in the above snippet is idealised and does not represent any known library. golang-github-canonical-candid-1.12.3/docs/mfa.md000066400000000000000000000025211457263123000215120ustar00rootroot00000000000000# Multi-Factor Authentication Candid supports WebAuthn multi-factor authentication that can be configured for the static and LDAP identity providers. ## Supported browsers WebAuthn is currently supported in Google Chrome, Mozilla Firefox, Microsoft Edge and Apple Safari (preview) web browsers, as well as Windows 10 and Android platforms. For more info see [link](https://caniuse.com/?search=webauthn). ## Supported authenticators Candid supports WebAuthn multi-factor authentications, which requires uses to register an external authenticator that supports [FIDO2](https://fidoalliance.org/fido2/fido2-web-authentication-webauthn/) such as Yubikey 5. The first time a user logs in using an identity provider that is configured to require MFA, the user will be required to register an external authenticator. Following successful registration the user will be able to register multiple other authenticators. On subsequent logins user will be required to present one of the registered authenticators before completing the login process. ### Lost authenticators Should the user lose all registered authenticators, the Candid admin can user the **clear-mfa-credentials** command which will de-register all user's authenticators. Next time the user will be required to register a new authenticator. Example: > candid clear-mfa-credentials \ golang-github-canonical-candid-1.12.3/embed.go000066400000000000000000000004401457263123000210760ustar00rootroot00000000000000// Copyright 2021 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // +build go1.16 package candid import "embed" // ResourceFS contains embeded resource files (templates and static // content). //go:embed static //go:embed templates var ResourceFS embed.FS golang-github-canonical-candid-1.12.3/go.mod000066400000000000000000000164371457263123000206160ustar00rootroot00000000000000module github.com/canonical/candid require ( github.com/coreos/go-oidc v0.0.0-20170119174436-2cc7913f9f6f github.com/duo-labs/webauthn v0.0.0-20220815211337-00c9fb5711f5 github.com/frankban/quicktest v1.14.4 github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1 github.com/gomodule/oauth1 v0.2.0 github.com/google/go-cmp v0.5.9 github.com/google/uuid v1.3.0 github.com/gorilla/handlers v1.5.1 github.com/juju/aclstore/v2 v2.1.0 github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c github.com/juju/cmd/v3 v3.0.0-20210809234809-65029dab4cd0 github.com/juju/gnuflag v1.0.0 github.com/juju/loggo v1.0.0 github.com/juju/mgo/v2 v2.0.0-20220111072304-f200228f1090 github.com/juju/mgotest v1.0.3 github.com/juju/names/v4 v4.0.0-20200929085019-be23e191fee0 github.com/juju/persistent-cookiejar v0.0.0-20170428161559-d67418f14c93 github.com/juju/postgrestest v1.1.1 github.com/juju/qthttptest v0.1.3 github.com/juju/schema v1.0.1-0.20190814234152-1f8aaeef0989 github.com/juju/simplekv v1.1.0 github.com/juju/testing v0.0.0-20210302031854-2c7ee8570c07 github.com/juju/usso v1.0.1 github.com/juju/utils/v2 v2.0.0-20210305225158-eedbe7b6b3e2 github.com/julienschmidt/httprouter v1.3.0 github.com/lib/pq v1.10.7 github.com/mhilton/openid v0.0.0-20150511103207-7922a4e937d8 github.com/prometheus/client_golang v1.14.0 github.com/yohcop/openid-go v1.0.0 golang.org/x/crypto v0.1.0 golang.org/x/net v0.7.0 golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225 gopkg.in/errgo.v1 v1.0.1 gopkg.in/goose.v1 v1.0.0-20161130145116-8f055ce635d6 gopkg.in/httprequest.v1 v1.2.1 gopkg.in/juju/environschema.v1 v1.0.1 gopkg.in/ldap.v2 v2.5.1 gopkg.in/macaroon.v2 v2.1.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531180850-df99d62fd42d gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 gopkg.in/yaml.v2 v2.4.0 launchpad.net/lpad v0.0.0-20131113112110-000000000065 ) require ( cloud.google.com/go/compute v1.9.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cloudflare/cfssl v1.6.1 // indirect github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect github.com/envoyproxy/protoc-gen-validate v0.6.1 // indirect github.com/felixge/httpsnoop v1.0.1 // indirect github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect github.com/fullstorydev/grpcurl v1.8.1 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/mock v1.5.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/certificate-transparency-go v1.1.2-0.20210511102531-373a877eec92 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jhump/protoreflect v1.8.2 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/juju/ansiterm v0.0.0-20210706145210-9283cdf370b5 // indirect github.com/juju/collections v0.0.0-20200605021417-0d0ec82b7271 // indirect github.com/juju/errors v0.0.0-20200330140219-3fe23663418f // indirect github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a // indirect github.com/juju/httpprof v0.0.0-20141217160036-14bf14c30767 // indirect github.com/juju/retry v0.0.0-20180821225755-9058e192b216 // indirect github.com/juju/version v0.0.0-20191219164919-81c1be00b9a6 // indirect github.com/juju/webbrowser v0.0.0-20160309143629-54b8c57083b4 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-isatty v0.0.13 // indirect github.com/mattn/go-runewidth v0.0.12 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pquerna/cachecontrol v0.0.0-20160421231612-c97913dcbd76 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/rogpeppe/fastuuid v1.2.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/soheilhy/cmux v0.1.5 // indirect github.com/spf13/cobra v1.1.3 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect github.com/urfave/cli v1.22.5 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xdg-go/stringprep v1.0.2 // indirect github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect go.etcd.io/bbolt v1.3.5 // indirect go.etcd.io/etcd/api/v3 v3.5.0-alpha.0 // indirect go.etcd.io/etcd/client/v2 v2.305.0-alpha.0 // indirect go.etcd.io/etcd/client/v3 v3.5.0-alpha.0 // indirect go.etcd.io/etcd/etcdctl/v3 v3.5.0-alpha.0 // indirect go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0 // indirect go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0 // indirect go.etcd.io/etcd/server/v3 v3.5.0-alpha.0 // indirect go.etcd.io/etcd/tests/v3 v3.5.0-alpha.0 // indirect go.etcd.io/etcd/v3 v3.5.0-alpha.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.7.0 // indirect go.uber.org/zap v1.16.0 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/sys v0.5.0 // indirect golang.org/x/term v0.5.0 // indirect golang.org/x/text v0.7.0 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect golang.org/x/tools v0.1.12 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220804142021-4e6b2dfa6612 // indirect google.golang.org/grpc v1.48.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect gopkg.in/retry.v1 v1.0.3 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect sigs.k8s.io/yaml v1.2.0 // indirect ) go 1.20 golang-github-canonical-candid-1.12.3/go.sum000066400000000000000000004414551457263123000206450ustar00rootroot00000000000000bazil.org/fuse v0.0.0-20180421153158-65cc252bf669/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= bitbucket.org/creachadair/shell v0.0.6/go.mod h1:8Qqi/cYk7vPnsOePHroKXDJYmb5x7ENhtiFtfZq8K+M= bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute v1.9.0 h1:ED/FP4xv8GJw63v556/ASNc1CeeLUO2Bs8nzaHchkHg= cloud.google.com/go/compute v1.9.0/go.mod h1:lWv1h/zUWTm/LozzfTJhBSkd6ShQq8la8VeeuOEGxfY= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/spanner v1.17.0/go.mod h1:+17t2ixFwRG4lWRwE+5kipDR9Ef07Jkmc8z0IbMDKUs= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= code.gitea.io/sdk/gitea v0.11.3/go.mod h1:z3uwDV/b9Ls47NGukYM9XhnHtqPh/J+t40lsUrR6JDY= contrib.go.opencensus.io/exporter/aws v0.0.0-20181029163544-2befc13012d0/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= contrib.go.opencensus.io/exporter/ocagent v0.5.0/go.mod h1:ImxhfLRpxoYiSq891pBrLVhN+qmP8BTVvdH2YLs7Gl0= contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw= contrib.go.opencensus.io/exporter/stackdriver v0.13.5/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-amqp-common-go/v2 v2.1.0/go.mod h1:R8rea+gJRuJR6QxTir/XuEd+YuKoUiazDC/N96FiDEU= github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= github.com/Azure/azure-sdk-for-go v29.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v30.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbIe3AcV9WZbAdpkoXOa0= github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= github.com/GeertJohan/go.rice v1.0.2/go.mod h1:af5vUNlDNkCjOZeSGFgIJxDje9qdjsO6hshx0gTmZt4= github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20191009163259-e802c2cb94ae/go.mod h1:mjwGPas4yKduTyubHvD1Atl9r1rUq8DfVy+gkVvZ+oo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= github.com/apache/beam v2.28.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ= github.com/apex/logs v0.0.4/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.19.45/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.25.11/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw= github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s= github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY= github.com/cloudflare/cfssl v1.6.1 h1:aIOUjpeuDJOpWjVJFP2ByplF53OgqG8I1S40Ggdlk3g= github.com/cloudflare/cfssl v1.6.1/go.mod h1:ENhCj4Z17+bY2XikpxVmTHDg/C2IsG2Q0ZBeXpAqhCk= github.com/cloudflare/redoctober v0.0.0-20201013214028-99c99a8e7544/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210322005330-6414d713912e/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 h1:zH8ljVhhq7yC0MIeUL/IviMtY8hx2mK8cN9wEYb8ggw= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5 h1:xD/lrqdvwsc+O2bjSSi3YqY73Ke3LAiSCx49aCesA0E= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4 h1:Lap807SXTH5tri2TivECb/4abUkMZC9zRoLarvcKDqs= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-oidc v0.0.0-20170119174436-2cc7913f9f6f h1:M6NCFw9bacbe5kX3UgfMJIVQX8lGcW8PrjDu7mlAVGE= github.com/coreos/go-oidc v0.0.0-20170119174436-2cc7913f9f6f/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/daaku/go.zipexe v1.0.1/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/duo-labs/webauthn v0.0.0-20220815211337-00c9fb5711f5 h1:BaeJtFDlto/NjX9t730OebRRJf2P+t9YEDz3ur18824= github.com/duo-labs/webauthn v0.0.0-20220815211337-00c9fb5711f5/go.mod h1:Jcj7rFNlTknb18v9jpSA58BveX2LDhXqaoy+6YV1N9g= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 h1:xvqufLtNVwAhN8NMyWklVgxnWohi+wtMGQMhtxexlm0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.1 h1:4CF52PCseTFt4bE+Yk3dIpdVi7XWuPVMhPtm4FaIJPM= github.com/envoyproxy/protoc-gen-validate v0.6.1/go.mod h1:txg5va2Qkip90uYoSKH+nkAAmXrb2j3iq4FLwdrCbXQ= github.com/etcd-io/gofail v0.0.0-20190801230047-ad7f989257ca/go.mod h1:49H/RkXP8pKaZy4h0d+NW16rSLhyVBt4o6VLJbmOqDE= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= github.com/frankban/quicktest v1.1.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= github.com/frankban/quicktest v1.1.1/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= github.com/frankban/quicktest v1.5.0/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fullstorydev/grpcurl v1.8.0/go.mod h1:Mn2jWbdMrQGJQ8UD62uNyMumT2acsZUCkZIqFxsQf1o= github.com/fullstorydev/grpcurl v1.8.1 h1:Pp648wlTTg3OKySeqxM5pzh8XF6vLqrm8wRq66+5Xo0= github.com/fullstorydev/grpcurl v1.8.1/go.mod h1:3BWhvHZwNO7iLXaQlojdg5NA6SxUDePli4ecpK1N7gw= github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1 h1:uvQJoKTHrFFu8zxoaopNKedRzwdy3+8H72we4T/5cGs= github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1/go.mod h1:H59IYeChwvD1po3dhGUPvq5na+4NVD7SJlbhGKvslr0= github.com/go-macaroon-bakery/macaroonpb v1.0.0 h1:It9exBaRMZ9iix1iJ6gwzfwsDE6ExNuwtAJ9e09v6XE= github.com/go-macaroon-bakery/macaroonpb v1.0.0/go.mod h1:UzrGOcbiwTXISFP2XDLDPjfhMINZa+fX/7A2lMd31zc= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/oauth1 v0.2.0 h1:/nNHAD99yipOEspQFbAnNmwGTZ1UNXiD/+JLxwx79fo= github.com/gomodule/oauth1 v0.2.0/go.mod h1:4r/a8/3RkhMBxJQWL5qzbOEcaQmNPIkNoI7P8sXeI08= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/certificate-transparency-go v1.1.2-0.20210422104406-9f33727a7a18/go.mod h1:6CKh9dscIRoqc2kC6YUFICHZMT9NrClyPrRVFrdw1QQ= github.com/google/certificate-transparency-go v1.1.2-0.20210511102531-373a877eec92 h1:806qveZBQtRNHroYHyg6yrsjqBJh9kIB4nfmB8uJnak= github.com/google/certificate-transparency-go v1.1.2-0.20210511102531-373a877eec92/go.mod h1:kXWPsHVPSKVuxPPG69BRtumCbAW537FydV/GH89oBhM= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOmkVoJOpwnS0wfdsJCV9CoD5nJYsHoFk/0CrTK4M= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/licenseclassifier v0.0.0-20210325184830-bb04aff29e72/go.mod h1:qsqn2hxC+vURpyBRygGUuinTO42MFRLcsmQ/P8v94+M= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/trillian v1.3.14-0.20210409160123-c5ea3abd4a41/go.mod h1:1dPv0CUjNQVFEDuAUFhZql16pw/VlPgaX8qj+g5pVzQ= github.com/google/trillian v1.3.14-0.20210428093031-b4ddea2e86b1/go.mod h1:FdIJX+NoDk/dIN2ZxTyz5nAJWgf+NSSSriPAMThChTY= github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s= github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/goreleaser/goreleaser v0.134.0/go.mod h1:ZT6Y2rSYa6NxQzIsdfWWNWAlYGXGbreo66NmE+3X3WQ= github.com/goreleaser/nfpm v1.2.1/go.mod h1:TtWrABZozuLOttX2uDlYyECfQX7x5XYkVxhjYcR6G9w= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.2/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4GkDEdKCRJduHpTxT3/jcw= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.6.1/go.mod h1:RZQ/lnuN+zqeRVpQigTwO6o0AJUkxbnSnpuG7toUTG4= github.com/jhump/protoreflect v1.8.2 h1:k2xE7wcUomeqwY0LDCYA16y4WWfyTcMx5mKhk0d4ua0= github.com/jhump/protoreflect v1.8.2/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/aclstore/v2 v2.1.0 h1:aIZmJw4cLAKLKlvU0yaM6b2HBSRgFjILJ2pqtZnp7aU= github.com/juju/aclstore/v2 v2.1.0/go.mod h1:c2AibPW3K+JKHmkqlvPET9hfbe2vPa3e1GuNw76NlKM= github.com/juju/ansiterm v0.0.0-20160907234532-b99631de12cf/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/juju/ansiterm v0.0.0-20210706145210-9283cdf370b5 h1:Q5klzs6BL5FkassBX65t+KkG0XjYcjxEm+GNcQAsuaw= github.com/juju/ansiterm v0.0.0-20210706145210-9283cdf370b5/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/juju/clock v0.0.0-20180808021310-bab88fc67299/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c h1:3UvYABOQRhJAApj9MdCN+Ydv841ETSoy6xLzdmmr/9A= github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= github.com/juju/cmd v0.0.0-20171107070456-e74f39857ca0/go.mod h1:yWJQHl73rdSX4DHVKGqkAip+huBslxRwS8m9CrOLq18= github.com/juju/cmd/v3 v3.0.0-20210809234809-65029dab4cd0 h1:WQ0BXDhwzasiRN7CxrCrsErybzNgNK1ouP6U5CXjCnU= github.com/juju/cmd/v3 v3.0.0-20210809234809-65029dab4cd0/go.mod h1:wFYE8X/PTiXeQoxBB4xN8rdx7e5/pr7uOlAdas0g7x8= github.com/juju/collections v0.0.0-20200605021417-0d0ec82b7271 h1:4R626WTwa7pRYQFiIRLVPepMhm05eZMEx+wIurRnMLc= github.com/juju/collections v0.0.0-20200605021417-0d0ec82b7271/go.mod h1:5XgO71dV1JClcOJE+4dzdn4HrI5LiyKd7PlVG6eZYhY= github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/errors v0.0.0-20180726005433-812b06ada177/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/errors v0.0.0-20190207033735-e65537c515d7/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/errors v0.0.0-20200330140219-3fe23663418f h1:MCOvExGLpaSIzLYB4iQXEHP4jYVU6vmzLNQPdMVrxnM= github.com/juju/errors v0.0.0-20200330140219-3fe23663418f/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/juju/gnuflag v1.0.0 h1:E6OmPEi2nqJYanlIw7a+bUF+FDiK3uSBHftRmQi3muQ= github.com/juju/gnuflag v1.0.0/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a h1:45JtCyuNYE+QN9aPuR1ID9++BQU+NMTMudHSuaK0Las= github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a/go.mod h1:RVHtZuvrpETIepiNUrNlih2OynoFf1eM6DGC6dloXzk= github.com/juju/httpprof v0.0.0-20141217160036-14bf14c30767 h1:COsaGcfAONDdIDnGS8yFdxOyReP7zKQEr7jFzCHKDkM= github.com/juju/httpprof v0.0.0-20141217160036-14bf14c30767/go.mod h1:+MaLYz4PumRkkyHYeXJ2G5g5cIW0sli2bOfpmbaMV/g= github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20190212223446-d976af380377/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20200526014432-9ce3a2e09b5e/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4/go.mod h1:NIXFioti1SmKAlKNuUwbMenNdef59IF52+ZzuOmHYkg= github.com/juju/loggo v1.0.0 h1:Y6ZMQOGR9Aj3BGkiWx7HBbIx6zNwNkxhVNOHU2i1bl0= github.com/juju/loggo v1.0.0/go.mod h1:NIXFioti1SmKAlKNuUwbMenNdef59IF52+ZzuOmHYkg= github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208/go.mod h1:0OChplkvPTZ174D2FYZXg4IB9hbEwyHkD+zT+/eK+Fg= github.com/juju/mgo/v2 v2.0.0-20220111072304-f200228f1090 h1:zX5GoH3Jp8k1EjUFkApu/YZAYEn0PYQfg/U6IDyNyYs= github.com/juju/mgo/v2 v2.0.0-20220111072304-f200228f1090/go.mod h1:N614SE0a4e+ih2rg96Vi2PeC3cTpUOWgCTv3Cgk974c= github.com/juju/mgotest v1.0.2/go.mod h1:04v1Xi2RiTO3h77YWtaXB2LAaGRSSi+Vl4hOV1coD0k= github.com/juju/mgotest v1.0.3 h1:3UIS2cOSzE6qz/dtiLAaQew5AKYw/bRb++/lsB522HI= github.com/juju/mgotest v1.0.3/go.mod h1:Dnzi6seljG9GoZpqFdTqRV3ybB3UcIj+H8iQqy1so1A= github.com/juju/mutex v0.0.0-20171110020013-1fe2a4bf0a3a/go.mod h1:Y3oOzHH8CQ0Ppt0oCKJ2JFO81/EsWenH5AEqigLH+yY= github.com/juju/names/v4 v4.0.0-20200929085019-be23e191fee0 h1:SqeiFgQd+LhYUHg3fpRBaR4iK8a0MviLC7v8TJuiJ20= github.com/juju/names/v4 v4.0.0-20200929085019-be23e191fee0/go.mod h1:gdlBx0aNufAMkEx3GjT8Yz4MChs3oVjCp/nEz/PrTX4= github.com/juju/persistent-cookiejar v0.0.0-20170428161559-d67418f14c93 h1:nlmpG1/Pv5elsi69wXhLkBhefGPE19bOCJ/xVwovl7A= github.com/juju/persistent-cookiejar v0.0.0-20170428161559-d67418f14c93/go.mod h1:zrbmo4nBKaiP/Ez3F67ewkMbzGYfXyMvRtbOfuAwG0w= github.com/juju/postgrestest v1.1.0/go.mod h1:/n17Y2T6iFozzXwSCO0JYJ5gSiz2caEtSwAjh/uLXDM= github.com/juju/postgrestest v1.1.1 h1:N5Lys2LN1/JWh17X3MsGQFVpuFnOmwDbmSQUwIRyxRE= github.com/juju/postgrestest v1.1.1/go.mod h1:/n17Y2T6iFozzXwSCO0JYJ5gSiz2caEtSwAjh/uLXDM= github.com/juju/qthttptest v0.1.1/go.mod h1:aTlAv8TYaflIiTDIQYzxnl1QdPjAg8Q8qJMErpKy6A4= github.com/juju/qthttptest v0.1.3 h1:M0HdpwsK/UTHRGRcIw5zvh5z+QOgdqyK+ecDMN+swwM= github.com/juju/qthttptest v0.1.3/go.mod h1:2gayREyVSs/IovPmwYAtU+HZzuhDjytJQRRLzPTtDYE= github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/juju/retry v0.0.0-20151029024821-62c620325291/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= github.com/juju/retry v0.0.0-20160928201858-1998d01ba1c3/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= github.com/juju/retry v0.0.0-20180821225755-9058e192b216 h1:/eQL7EJQKFHByJe3DeE8Z36yqManj9UY5zppDoQi4FU= github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= github.com/juju/schema v1.0.0/go.mod h1:Y+ThzXpUJ0E7NYYocAbuvJ7vTivXfrof/IfRPq/0abI= github.com/juju/schema v1.0.1-0.20190814234152-1f8aaeef0989 h1:qx1Zh1bnHHVIMmRxq0fehYk7npCG50GhUwEkYeUg/t4= github.com/juju/schema v1.0.1-0.20190814234152-1f8aaeef0989/go.mod h1:Y+ThzXpUJ0E7NYYocAbuvJ7vTivXfrof/IfRPq/0abI= github.com/juju/simplekv v1.1.0 h1:3j2a817FVp1uwwc7Y0+f9Bok2HSBoyLPJKOAzlQ/z0o= github.com/juju/simplekv v1.1.0/go.mod h1:OZjCrSxeKfEpNNp3JtM4B8NOVR4EJTffgvRY1qpPZ+w= github.com/juju/testing v0.0.0-20180402130637-44801989f0f7/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/juju/testing v0.0.0-20180517134105-72703b1e95eb/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/juju/testing v0.0.0-20210302031854-2c7ee8570c07 h1:6QA3rIUc3TBPbv8zWa2KQ2TWn6gsn1EU0UhwRi6kOhA= github.com/juju/testing v0.0.0-20210302031854-2c7ee8570c07/go.mod h1:7lxZW0B50+xdGFkvhAb8bwAGt6IU87JB1H9w4t8MNVM= github.com/juju/usso v1.0.1 h1:zyQhSUJnhFZdPqVAmPeqXYlnYXv+i0Cp1Ii+aziMXGs= github.com/juju/usso v1.0.1/go.mod h1:3cvBcGVmWXyHhrBHBQtpNBzca/JRg4S5XH88Hj/NsYA= github.com/juju/utils v0.0.0-20180424094159-2000ea4ff043/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= github.com/juju/utils v0.0.0-20180619112806-c746c6e86f4f/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= github.com/juju/utils v0.0.0-20200116185830-d40c2fe10647 h1:wQpkHVbIIpz1PCcLYku9KFWsJ7aEMQXHBBmLy3tRBTk= github.com/juju/utils v0.0.0-20200116185830-d40c2fe10647/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= github.com/juju/utils/v2 v2.0.0-20200923005554-4646bfea2ef1/go.mod h1:fdlDtQlzundleLLz/ggoYinEt/LmnrpNKcNTABQATNI= github.com/juju/utils/v2 v2.0.0-20210305225158-eedbe7b6b3e2 h1:E7BgV8lczMmMqMtXdOis5BPEDu6bSG1D6K7SHEq7hEw= github.com/juju/utils/v2 v2.0.0-20210305225158-eedbe7b6b3e2/go.mod h1:p35YIk2Pj1lxjhWuYsYbKvMpJ/iX9F8DBgJkNbGF0nQ= github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= github.com/juju/version v0.0.0-20191219164919-81c1be00b9a6 h1:nrqc9b4YKpKV4lPI3GPPFbo5FUuxkWxgZE2Z8O4lgaw= github.com/juju/version v0.0.0-20191219164919-81c1be00b9a6/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= github.com/juju/webbrowser v0.0.0-20160309143629-54b8c57083b4 h1:go1FDIXkFL8AUWgJ7B68rtFWCidyrMfZH9x3xwFK74s= github.com/juju/webbrowser v0.0.0-20160309143629-54b8c57083b4/go.mod h1:G6PCelgkM6cuvyD10iYJsjLBsSadVXtJ+nBxFAxE2BU= github.com/julienschmidt/httprouter v0.0.0-20151013225520-77a895ad01eb/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.1.1-0.20151013225520-77a895ad01eb/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/kisom/goutils v1.4.3/go.mod h1:Lp5qrquG7yhYnWzZCI/68Pa/GpFynw//od6EkGnWpac= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/go-gypsy v1.0.0/go.mod h1:chkXM0zjdpXOiqkCW1XcCHDfjfk14PH2KKkQWxfJUcU= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lunixbochs/vtclean v0.0.0-20160125035106-4fbf7632a2c6/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lyft/protoc-gen-star v0.5.1/go.mod h1:9toiA3cC7z5uVbODF7kEQ91Xn7XNFkVUl+SrEe+ZORU= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/masterzen/azure-sdk-for-go v3.2.0-beta.0.20161014135628-ee4f0065d00c+incompatible/go.mod h1:mf8fjOu33zCqxUjuiU3I8S1lJMyEAlH+0F2+M5xl3hE= github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= github.com/masterzen/winrm v0.0.0-20161014151040-7a535cd943fc/go.mod h1:CfZSN7zwz5gJiFhZJz49Uzk7mEBHIceWmbFmYx7Hf7E= github.com/masterzen/xmlpath v0.0.0-20140218185901-13f4951698ad/go.mod h1:A0zPC53iKKKcXYxr4ROjpQRQ5FgJXtelNdSmHHuq/tY= github.com/mattn/go-colorable v0.0.6/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mhilton/openid v0.0.0-20150511103207-7922a4e937d8 h1:1MdhcwDp+uIJPcQPkVuwCNY43NMlElr/tIJ40HjPlpE= github.com/mhilton/openid v0.0.0-20150511103207-7922a4e937d8/go.mod h1:Alv076OXc0MA78hV0BTU06FTh1Q9sWKk3Ru20SykbTA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= github.com/mwitkow/go-proto-validators v0.2.0/go.mod h1:ZfA1hW+UH/2ZHOWvQ3HnQaU0DtnpXu850MZiy+YUgcc= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso= github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/cachecontrol v0.0.0-20160421231612-c97913dcbd76 h1:O60OlfVScwx/OixpMy8gIPeKNIN3bI9BrOuTIUexlbc= github.com/pquerna/cachecontrol v0.0.0-20160421231612-c97913dcbd76/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/common v0.24.0/go.mod h1:H6QK/N6XVT42whUeIdI3dp36w49c+/iMDk7UAI2qm7Q= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/pseudomuto/protoc-gen-doc v1.4.1/go.mod h1:exDTOVwqpp30eV/EDPFLZy3Pwr2sn6hBC1WIYH/UbIg= github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a h1:3QH7VyOaaiUHNrA9Se4YQIRkDTCw1EJls9xTUCaCeRM= github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.5-0.20210205191134-5ec6847320e5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.3.4/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/weppos/publicsuffix-go v0.13.1-0.20210123135404-5fd73613514e/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yohcop/openid-go v1.0.0 h1:EciJ7ZLETHR3wOtxBvKXx9RV6eyHZpCaSZ1inbBaUXE= github.com/yohcop/openid-go v1.0.0/go.mod h1:/408xiwkeItSPJZSTPF7+VtZxPkPrRRpRNK2vjGh6yI= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= github.com/zmap/zcrypto v0.0.0-20210123152837-9cf5beac6d91/go.mod h1:R/deQh6+tSWlgI9tb4jNmXxn8nSCabl5ZQsBX9//I/E= github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc/go.mod h1:FM4U1E3NzlNMRnSUTU3P1UdukWhYGifqEsjk9fn7BCk= github.com/zmap/zlint/v3 v3.1.0/go.mod h1:L7t8s3sEKkb0A2BxGy1IWrxt1ZATa1R4QfJZaQOD3zU= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.etcd.io/etcd/api/v3 v3.5.0-alpha.0 h1:+e5nrluATIy3GP53znpkHMFzPTHGYyzvJGFCbuI6ZLc= go.etcd.io/etcd/api/v3 v3.5.0-alpha.0/go.mod h1:mPcW6aZJukV6Aa81LSKpBjQXTWlXB5r74ymPoSWa3Sw= go.etcd.io/etcd/client/v2 v2.305.0-alpha.0 h1:jZepGpOeJATxsbMNBZczDS2jHdK/QVHM1iPe9jURJ8o= go.etcd.io/etcd/client/v2 v2.305.0-alpha.0/go.mod h1:kdV+xzCJ3luEBSIeQyB/OEKkWKd8Zkux4sbDeANrosU= go.etcd.io/etcd/client/v3 v3.5.0-alpha.0 h1:dr1EOILak2pu4Nf5XbRIOCNIBjcz6UmkQd7hHRXwxaM= go.etcd.io/etcd/client/v3 v3.5.0-alpha.0/go.mod h1:wKt7jgDgf/OfKiYmCq5WFGxOFAkVMLxiiXgLDFhECr8= go.etcd.io/etcd/etcdctl/v3 v3.5.0-alpha.0 h1:odMFuQQCg0UmPd7Cyw6TViRYv9ybGuXuki4CusDSzqA= go.etcd.io/etcd/etcdctl/v3 v3.5.0-alpha.0/go.mod h1:YPwSaBciV5G6Gpt435AasAG3ROetZsKNUzibRa/++oo= go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0 h1:3yLUEC0nFCxw/RArImOyRUI4OAFbg4PFpBbAhSNzKNY= go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0/go.mod h1:tV31atvwzcybuqejDoY3oaNRTtlD2l/Ot78Pc9w7DMY= go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0 h1:DvYJotxV9q1Lkn7pknzAbFO/CLtCVidCr2K9qRLJ8pA= go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0/go.mod h1:FAwse6Zlm5v4tEWZaTjmNhe17Int4Oxbu7+2r0DiD3w= go.etcd.io/etcd/server/v3 v3.5.0-alpha.0 h1:fYv7CmmdyuIu27UmKQjS9K/1GtcCa+XnPKqiKBbQkrk= go.etcd.io/etcd/server/v3 v3.5.0-alpha.0/go.mod h1:tsKetYpt980ZTpzl/gb+UOJj9RkIyCb1u4wjzMg90BQ= go.etcd.io/etcd/tests/v3 v3.5.0-alpha.0 h1:UcRoCA1FgXoc4CEM8J31fqEvI69uFIObY5ZDEFH7Znc= go.etcd.io/etcd/tests/v3 v3.5.0-alpha.0/go.mod h1:HnrHxjyCuZ8YDt8PYVyQQ5d1ZQfzJVEtQWllr5Vp/30= go.etcd.io/etcd/v3 v3.5.0-alpha.0 h1:ZuqKJkD2HrzFUj8IB+GLkTMKZ3+7mWx172vx6F1TukM= go.etcd.io/etcd/v3 v3.5.0-alpha.0/go.mod h1:JZ79d3LV6NUfPjUxXrpiFAYcjhT+06qqw+i28snx8To= go.opencensus.io v0.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= gocloud.dev v0.19.0/go.mod h1:SmKwiR8YwIMMJvQBKLsC3fHNyMwXLw3PMDO+VVteJMI= golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 h1:+jnHzr9VPj32ykQVai5DNahi9+NSp7yYuCsl5eAQtL0= golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190620070143-6f217b454f45/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191119060738-e882bf8e40c2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191118222007-07fc4c7f2b98/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201014170642-d1624618ad65/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.45.0/go.mod h1:ISLIJCedJolbZvDfAk+Ctuq5hf+aJ33WgtUsfyFoLXA= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181107211654-5fc9ac540362/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190620144150-6af8c5fc6601/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210331142528-b7513248f0ba/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210510173355-fb37daa5cd7a/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20220804142021-4e6b2dfa6612 h1:NX3L5YesD5qgxxrPHdKqHH38Ao0AG6poRXG+JljPsGU= google.golang.org/genproto v0.0.0-20220804142021-4e6b2dfa6612/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225 h1:JBwmEvLfCqgPcIq8MjVMQxsF3LVL4XG/HH0qiG0+IFY= gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v1 v1.0.0-20161222125816-442357a80af5/go.mod h1:u0ALmqvLRxLI95fkdCEWrE6mhWYZW1aMOJHp5YXLHTg= gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk= gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso= gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/goose.v1 v1.0.0-20161130145116-8f055ce635d6 h1:deAcL0D9tqowC4zIlaFW36XVeqsNBZEBxi6d4pHIJAI= gopkg.in/goose.v1 v1.0.0-20161130145116-8f055ce635d6/go.mod h1:ZM14ECObhzpclsfV8uWsmADh80xveEXRV35GG4g+DHY= gopkg.in/httprequest.v1 v1.1.1/go.mod h1:/CkavNL+g3qLOrpFHVrEx4NKepeqR4XTZWNj4sGGjz0= gopkg.in/httprequest.v1 v1.1.2/go.mod h1:/CkavNL+g3qLOrpFHVrEx4NKepeqR4XTZWNj4sGGjz0= gopkg.in/httprequest.v1 v1.2.1 h1:pEPLMdF/gjWHnKxLpuCYaHFjc8vAB2wrYjXrqDVC16E= gopkg.in/httprequest.v1 v1.2.1/go.mod h1:x2Otw96yda5+8+6ZeWwHIJTFkEHWP/qP8pJOzqEtWPM= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/juju/environschema.v1 v1.0.1 h1:eXQQsfSJykpp1Kz79pVmKWE6G5yzKuiCqkR01LHFVS0= gopkg.in/juju/environschema.v1 v1.0.1/go.mod h1:WTgU3KXKCVoO9bMmG/4KHzoaRvLeoxfjArpgd1MGWFA= gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU= gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk= gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI= gopkg.in/macaroon.v2 v2.1.0/go.mod h1:OUb+TQP/OP0WOerC2Jp/3CwhIKyIa9kQjuc7H24e6/o= gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531180850-df99d62fd42d h1:OcCVYXNtNR0atgUveHvW25qtiCQv+2OqEUQ0SABShpE= gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531180850-df99d62fd42d/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/retry.v1 v1.0.2/go.mod h1:tLRIBNXxoKtalyAWBSIbHdWkIBN2x9jVEm5l0Z+BjXs= gopkg.in/retry.v1 v1.0.3 h1:a9CArYczAVv6Qs6VGoLMio99GEs7kY9UzSF9+LD+iGs= gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs= gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.4 h1:SadWOkti5uVN1FAMgxn165+Mw00fuQKyk4Gyn/inxNQ= honnef.co/go/tools v0.1.4/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54= launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= launchpad.net/lpad v0.0.0-20131113112110-000000000065 h1:+DBKrw8upWjmF2616hr/qKeWjP/Gd/Wvdxf9b6wv7lI= launchpad.net/lpad v0.0.0-20131113112110-000000000065/go.mod h1:cItrEkWN+F4fC6+HU9vEne57WkgMNe2aIWFFiYbvJ3M= launchpad.net/xmlpath v0.0.0-20130614043138-000000000004/go.mod h1:vqyExLOM3qBx7mvYRkoxjSCF945s0mbe7YynlKYXtsA= pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= golang-github-canonical-candid-1.12.3/idp/000077500000000000000000000000001457263123000202515ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/adfs/000077500000000000000000000000001457263123000211665ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/adfs/adfs.go000066400000000000000000000055531457263123000224420ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package adfs is an identity provider that authenticates with an ADFS // service. package adfs import ( oidc "github.com/coreos/go-oidc" "gopkg.in/errgo.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil/msgraph" "github.com/canonical/candid/idp/openid" ) func init() { idp.Register("adfs", func(unmarshal func(interface{}) error) (idp.IdentityProvider, error) { var p Params if err := unmarshal(&p); err != nil { return nil, errgo.Notef(err, "cannot unmarshal adfs parameters") } if p.URL == "" { return nil, errgo.Newf("url not specified") } if p.ClientID == "" { return nil, errgo.Newf("client-id not specified") } if p.ClientSecret == "" { return nil, errgo.Newf("client-secret not specified") } return NewIdentityProvider(p), nil }) } type Params struct { // Name is the name that will be given to the identity provider. Name string `yaml:"name"` // Description is the description that will be used with the // identity provider. If this is not set then Name will be used. Description string `yaml:"description"` // Icon contains the URL or path of an icon. Icon string `yaml:"icon"` // Domain is the domain with which all identities created by this // identity provider will be tagged (not including the @ separator). Domain string `yaml:"domain"` // URL is the URL of the Active Directory Federation Services // instance that is used to provide identities. OpenID Connect // discovery will be run on this URL to determine the required // service parameters. URL string `yaml:"url"` // ClientID contains the Application Id for the application. ClientID string `yaml:"client-id"` // ClientSecret contains a password type Application Secret for // the application. ClientSecret string `yaml:"client-secret"` // Hidden is set if the IDP should be hidden from interactive // prompts. Hidden bool `yaml:"hidden"` // MatchEmailAddr is a regular expression that is used to determine if // this identity provider can be used for a particular user email. MatchEmailAddr string `yaml:"match-email-addr"` } // NewIdentityProvider creates an ADFS identity provider with the // configuration defined by p. func NewIdentityProvider(p Params) idp.IdentityProvider { if p.Name == "" { p.Name = "adfs" } if p.Domain == "" { p.Domain = p.Name } return openid.NewOpenIDConnectIdentityProvider(openid.OpenIDConnectParams{ Name: p.Name, Issuer: p.URL, Domain: p.Domain, Description: p.Description, Icon: p.Icon, Scopes: []string{oidc.ScopeOpenID, "email", "profile"}, ClientID: p.ClientID, ClientSecret: p.ClientSecret, Hidden: p.Hidden, MatchEmailAddr: p.MatchEmailAddr, GroupsRetriever: &msgraph.MsGraphGroupsRetriever{}, }) } golang-github-canonical-candid-1.12.3/idp/adfs/adfs_test.go000066400000000000000000000030171457263123000234720ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package adfs_test import ( "testing" qt "github.com/frankban/quicktest" "gopkg.in/yaml.v2" "github.com/canonical/candid/config" ) var configTests = []struct { about string yaml string expectError string }{{ about: "good config", yaml: ` identity-providers: - type: adfs url: https://example.com client-id: client-001 client-secret: secret-001 `, }, { about: "no client-id", yaml: ` identity-providers: - type: adfs url: https://example.com client-secret: secret-001 `, expectError: `cannot unmarshal adfs configuration: client-id not specified`, }, { about: "no client-secret", yaml: ` identity-providers: - type: adfs url: https://example.com client-id: client-001 `, expectError: `cannot unmarshal adfs configuration: client-secret not specified`, }, { about: "no issuer", yaml: ` identity-providers: - type: adfs client-id: client-001 client-secret: secret-001 `, expectError: `cannot unmarshal adfs configuration: url not specified`, }} func TestConfig(t *testing.T) { c := qt.New(t) for _, test := range configTests { c.Run(test.about, func(c *qt.C) { var conf config.Config err := yaml.Unmarshal([]byte(test.yaml), &conf) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) return } c.Assert(err, qt.IsNil) c.Assert(conf.IdentityProviders, qt.HasLen, 1) c.Assert(conf.IdentityProviders[0].Name(), qt.Equals, "adfs") }) } } golang-github-canonical-candid-1.12.3/idp/agent/000077500000000000000000000000001457263123000213475ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/agent/agent.go000066400000000000000000000046741457263123000230070ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package agent is an identity provider that uses the agent authentication scheme. package agent import ( "context" "net/http" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/store" ) // IdentityProvider is the instance of the agent identity provider. // // Note: this identity provider will no longer be used, it is provided // for backwards-compatibility purposes only. The agent functionality is // now built in to the identity manager. var IdentityProvider idp.IdentityProvider = (*identityProvider)(nil) func init() { idp.Register("agent", func(func(interface{}) error) (idp.IdentityProvider, error) { return IdentityProvider, nil }) } // identityProvider allows login using pre-registered agent users. type identityProvider struct{} // Name gives the name of the identity provider (agent). func (*identityProvider) Name() string { return "agent" } // Domain returns "" as the agent identity provider will not create // users. func (*identityProvider) Domain() string { return "" } // Description gives a description of the identity provider. func (*identityProvider) Description() string { return "" } // IconURL returns the URL of an icon for the identity provider. func (*identityProvider) IconURL() string { return "" } // Interactive specifies that this identity provider is not interactive. func (*identityProvider) Interactive() bool { return false } // Hidden specifies that this identity provider is not hidden. func (*identityProvider) Hidden() bool { return false } // Init implements idp.IdentityProvider.Init by doing nothing. func (*identityProvider) Init(context.Context, idp.InitParams) error { return errgo.New("agent login IDP no longer supported") } // URL gets the login URL to use this identity provider. func (*identityProvider) URL(string) string { return "" } // SetInteraction implements idp.IdentityProvider.SetInteraction by doing // nothing. func (*identityProvider) SetInteraction(ierr *httpbakery.Error, dischargeID string) { } // Handle handles the agent login process. func (*identityProvider) Handle(context.Context, http.ResponseWriter, *http.Request) { } // GetGroups implements idp.IdentityProvider.GetGroups. func (*identityProvider) GetGroups(context.Context, *store.Identity) ([]string, error) { return nil, nil } golang-github-canonical-candid-1.12.3/idp/agent/agent_test.go000066400000000000000000000027401457263123000240360ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package agent_test import ( "testing" qt "github.com/frankban/quicktest" "gopkg.in/yaml.v2" "github.com/canonical/candid/config" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/agent" ) func TestConfig(t *testing.T) { c := qt.New(t) configYaml := ` identity-providers: - type: agent ` var conf config.Config err := yaml.Unmarshal([]byte(configYaml), &conf) c.Assert(err, qt.IsNil) c.Assert(conf.IdentityProviders, qt.HasLen, 1) c.Assert(conf.IdentityProviders[0].Name(), qt.Equals, "agent") } func TestName(t *testing.T) { c := qt.New(t) c.Assert(agent.IdentityProvider.Name(), qt.Equals, "agent") } func TestDescription(t *testing.T) { c := qt.New(t) c.Assert(agent.IdentityProvider.Description(), qt.Equals, "") } func TestIconURL(t *testing.T) { c := qt.New(t) c.Assert(agent.IdentityProvider.IconURL(), qt.Equals, "") } func TestInteractive(t *testing.T) { c := qt.New(t) c.Assert(agent.IdentityProvider.Interactive(), qt.Equals, false) } func TestHidden(t *testing.T) { c := qt.New(t) c.Assert(agent.IdentityProvider.Hidden(), qt.Equals, false) } func TestURL(t *testing.T) { c := qt.New(t) u := agent.IdentityProvider.URL("1") c.Assert(u, qt.Equals, "") } func TestInitProducesError(t *testing.T) { c := qt.New(t) err := agent.IdentityProvider.Init(nil, idp.InitParams{}) c.Assert(err, qt.ErrorMatches, "agent login IDP no longer supported") } golang-github-canonical-candid-1.12.3/idp/azure/000077500000000000000000000000001457263123000213775ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/azure/azure.go000066400000000000000000000047721457263123000230660ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package azure is an identity provider that authenticates with azure. package azure import ( oidc "github.com/coreos/go-oidc" "gopkg.in/errgo.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil/msgraph" "github.com/canonical/candid/idp/openid" ) func init() { idp.Register("azure", func(unmarshal func(interface{}) error) (idp.IdentityProvider, error) { var p Params if err := unmarshal(&p); err != nil { return nil, errgo.Notef(err, "cannot unmarshal azure parameters") } if p.ClientID == "" { return nil, errgo.Newf("client-id not specified") } if p.ClientSecret == "" { return nil, errgo.Newf("client-secret not specified") } return NewIdentityProvider(p), nil }) } type Params struct { // Name is the name that will be given to the identity provider. Name string `yaml:"name"` // Description is the description that will be used with the // identity provider. If this is not set then Name will be used. Description string `yaml:"description"` // Icon contains the URL or path of an icon. Icon string `yaml:"icon"` // Domain is the domain with which all identities created by this // identity provider will be tagged (not including the @ separator). Domain string `yaml:"domain"` // ClientID contains the Application Id for the application // registered at https://apps.dev.microsoft.com. ClientID string `yaml:"client-id"` // ClientSecret contains a password type Application Secret for // the application as generated on // https://apps.dev.microsoft.com. ClientSecret string `yaml:"client-secret"` // Hidden is set if the IDP should be hidden from interactive // prompts. Hidden bool `yaml:"hidden"` } // NewIdentityProvider creates an azure identity provider with the // configuration defined by p. func NewIdentityProvider(p Params) idp.IdentityProvider { if p.Name == "" { p.Name = "azure" } if p.Domain == "" { p.Domain = "azure" } if p.Icon == "" { p.Icon = "/static/images/icons/azure.svg" } return openid.NewOpenIDConnectIdentityProvider(openid.OpenIDConnectParams{ Name: p.Name, Issuer: "https://login.live.com", Description: p.Description, Icon: p.Icon, Domain: p.Domain, Scopes: []string{oidc.ScopeOpenID, "profile"}, ClientID: p.ClientID, ClientSecret: p.ClientSecret, Hidden: p.Hidden, GroupsRetriever: &msgraph.MsGraphGroupsRetriever{}, }) } golang-github-canonical-candid-1.12.3/idp/azure/azure_test.go000066400000000000000000000023741457263123000241210ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package azure_test import ( "testing" qt "github.com/frankban/quicktest" "gopkg.in/yaml.v2" "github.com/canonical/candid/config" ) var configTests = []struct { about string yaml string expectError string }{{ about: "good config", yaml: ` identity-providers: - type: azure client-id: client-001 client-secret: secret-001 `, }, { about: "no client-id", yaml: ` identity-providers: - type: azure client-secret: secret-001 `, expectError: `cannot unmarshal azure configuration: client-id not specified`, }, { about: "no client-secret", yaml: ` identity-providers: - type: azure client-id: client-001 `, expectError: `cannot unmarshal azure configuration: client-secret not specified`, }} func TestConfig(t *testing.T) { c := qt.New(t) for _, test := range configTests { c.Run(test.about, func(c *qt.C) { var conf config.Config err := yaml.Unmarshal([]byte(test.yaml), &conf) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) return } c.Assert(err, qt.IsNil) c.Assert(conf.IdentityProviders, qt.HasLen, 1) c.Assert(conf.IdentityProviders[0].Name(), qt.Equals, "azure") }) } } golang-github-canonical-candid-1.12.3/idp/config.go000066400000000000000000000027021457263123000220460ustar00rootroot00000000000000package idp import ( "gopkg.in/errgo.v1" ) // idps holds the registry of identity providers, indexed by idp type. var idps = make(map[string]func(func(interface{}) error) (IdentityProvider, error)) // Config allows an IdentityProvider instance to be unmarshaled from a // YAML configuration file. The "type" field determines which registered // provider is used for the unmarshaling. type Config struct { IdentityProvider } func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { var t struct { Type string } if err := unmarshal(&t); err != nil { return errgo.Notef(err, "cannot unmarshal identity provider type") } if idpf, ok := idps[t.Type]; ok { provider, err := idpf(unmarshal) if err != nil { return errgo.Notef(err, "cannot unmarshal %s configuration", t.Type) } c.IdentityProvider = provider return nil } return errgo.Newf("unrecognised identity provider type %q", t.Type) } // Register is used by identity providers to register a function that // can be used to unmarshal an identity provider type. When the identity // provider with the given name is used, f will be // called to unmarshal its parameters from YAML. Its argument will be an // unmarshalYAML function that can be used to unmarshal the configuration // parameters into its argument according to the rules specified in // gopkg.in/yaml.v2. func Register(idpType string, f func(func(interface{}) error) (IdentityProvider, error)) { idps[idpType] = f } golang-github-canonical-candid-1.12.3/idp/google/000077500000000000000000000000001457263123000215255ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/google/google.go000066400000000000000000000046641457263123000233420ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package google is an identity provider that authenticates with google. package google import ( oidc "github.com/coreos/go-oidc" "gopkg.in/errgo.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/openid" ) func init() { idp.Register("google", func(unmarshal func(interface{}) error) (idp.IdentityProvider, error) { var p Params if err := unmarshal(&p); err != nil { return nil, errgo.Notef(err, "cannot unmarshal google parameters") } if p.ClientID == "" { return nil, errgo.Newf("client-id not specified") } if p.ClientSecret == "" { return nil, errgo.Newf("client-secret not specified") } return NewIdentityProvider(p), nil }) } type Params struct { // Name is the name that will be given to the identity provider. Name string `yaml:"name"` // Description is the description that will be used with the // identity provider. If this is not set then Name will be used. Description string `yaml:"description"` // Icon contains the URL or path of an icon. Icon string `yaml:"icon"` // Domain is the domain with which all identities created by this // identity provider will be tagged (not including the @ separator). Domain string `yaml:"domain"` // ClientID contains the Application Id for the application // registered at // https://console.developers.google.com/apis/credentials. ClientID string `yaml:"client-id"` // ClientSecret contains a password type Application Secret for // the application as generated on // https://console.developers.google.com/apis/credentials. ClientSecret string `yaml:"client-secret"` // Hidden is set if the IDP should be hidden from interactive // prompts. Hidden bool `yaml:"hidden"` } // NewIdentityProvider creates a google identity provider with the // configuration defined by p. func NewIdentityProvider(p Params) idp.IdentityProvider { if p.Name == "" { p.Name = "google" } if p.Domain == "" { p.Domain = "google" } if p.Icon == "" { p.Icon = "/static/images/icons/google.svg" } return openid.NewOpenIDConnectIdentityProvider(openid.OpenIDConnectParams{ Name: p.Name, Issuer: "https://accounts.google.com", Domain: p.Domain, Description: p.Description, Icon: p.Icon, Scopes: []string{oidc.ScopeOpenID, "email"}, ClientID: p.ClientID, ClientSecret: p.ClientSecret, Hidden: p.Hidden, }) } golang-github-canonical-candid-1.12.3/idp/google/google_test.go000066400000000000000000000024031457263123000243660ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package google_test import ( "testing" qt "github.com/frankban/quicktest" "gopkg.in/yaml.v2" "github.com/canonical/candid/config" ) var configTests = []struct { about string yaml string expectError string }{{ about: "good config", yaml: ` identity-providers: - type: google client-id: client-001 client-secret: secret-001 `, }, { about: "no client-id", yaml: ` identity-providers: - type: google client-secret: secret-001 `, expectError: `cannot unmarshal google configuration: client-id not specified`, }, { about: "no client-secret", yaml: ` identity-providers: - type: google client-id: client-001 `, expectError: `cannot unmarshal google configuration: client-secret not specified`, }} func TestConfig(t *testing.T) { c := qt.New(t) for _, test := range configTests { c.Run(test.about, func(c *qt.C) { var conf config.Config err := yaml.Unmarshal([]byte(test.yaml), &conf) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) return } c.Assert(err, qt.IsNil) c.Assert(conf.IdentityProviders, qt.HasLen, 1) c.Assert(conf.IdentityProviders[0].Name(), qt.Equals, "google") }) } } golang-github-canonical-candid-1.12.3/idp/idp.go000066400000000000000000000135611457263123000213620ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package idp defines the API provided by all identity providers. package idp import ( "context" "html/template" "net/http" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/simplekv" "github.com/canonical/candid/idp/idputil/secret" "github.com/canonical/candid/store" ) // A DischargeTokenCreator is used by the identity providers to create a // new httpbakery.DischargeToken for authenticated identity. type DischargeTokenCreator interface { // DischargeToken creates a new httpbakery.DischargeToken for the // given identity. DischargeToken(ctx context.Context, id *store.Identity) (*httpbakery.DischargeToken, error) } // A VisitCompleter is used by the identity providers to finish login // visit attempts. type VisitCompleter interface { // Success is used by an identity provider to indicate that a // successful login has been completed for the given identity. Success(ctx context.Context, w http.ResponseWriter, req *http.Request, dischargeID string, id *store.Identity) // Failure is used by an identity provider to indicate that a // login attempt has failed with the specified error. Failure(ctx context.Context, w http.ResponseWriter, req *http.Request, dischargeID string, err error) // RedirectFailure redirects to the given returnTo address with the given error. RedirectFailure(ctx context.Context, w http.ResponseWriter, req *http.Request, returnTo, state string, err error) // RedirectSuccess redirects to the given returnTo address // providing a code which can be used by the client to obtain a // disharge token for the given id. RedirectSuccess(ctx context.Context, w http.ResponseWriter, req *http.Request, returnTo, state string, id *store.Identity) // RedirectMFA redirects the user to the multi-factor login flow. RedirectMFA(ctx context.Context, w http.ResponseWriter, req *http.Request, requireMFA bool, returnTo, returnToState, state string, id *store.Identity) } // InitParams are passed to the identity provider to initialise it. type InitParams struct { // Store contains the identity store being used in the identity // server. Store store.Store // KeyValueStore contains a store that the provider may use to // store additional data that is not related to identities. KeyValueStore simplekv.Store // Oven contains an oven that may be used in the identity // provider to mint new macaroons. Oven *bakery.Oven // Codec contains the codec used to encode/decode session cookies // in the login flow. Codec *secret.Codec // Location contains the root location of the candid server. Location string // URLPrefix contains the prefix of all requests to the Handle // method. The URL.Path parameter in the request passed to handle // will contain only the part after this prefix. URLPrefix string // DischargeTokenCreator is the DischargeTokenCreator that the identity // provider should use to create discharge tokens. DischargeTokenCreator DischargeTokenCreator // VisitCompleter is the LoginCompleter that the identity // provider should use to complete visit requests. VisitCompleter VisitCompleter // Template contains the templates loaded in the identity server. Template *template.Template // SkipLocationForCookiePaths instructs if the Cookie Paths are to // be set relative to the Location Path or not. SkipLocationForCookiePaths bool } // IdentityProvider is the interface that is satisfied by all identity providers. type IdentityProvider interface { // Name is the short name for the identity provider, this will // appear in urls. Name() string // Domain is the domain in which this identity provider will // create users. Domain() string // Description is a name for the identity provider used to show // end users. Description() string // IconURL returns the URL of an icon image that represents the // identity provider. IconURL() string // Interactive indicates whether login is provided by the end // user interacting directly with the identity provider (usually // through a web browser). Interactive() bool // Hidden indicates that the IDP should not be listed on the // interactive login page, unless it has specifically been // requested (via a domain). Hidden() bool // Init is used to perform any one time initialization tasks that // are needed for the identity provider. Init is called once by // the identity manager once it has determined the identity // providers final location, any initialization tasks that depend // on having access to the final URL, or the per identity // provider database should be performed here. Init(ctx context.Context, params InitParams) error // URL returns the URL to use to attempt a login with this // identity provider. If the identity provider is interactive // then the user will be redirected to the URL. Otherwise the URL // is returned in the response to a request for login methods. // The given state value should be round-tripped through the // login interaction and used to verify the login when it // completes. URL(state string) string // SetInteraction adds interaction information for this identity // provider to the given interaction required error. SetInteraction(ierr *httpbakery.Error, dischargeID string) // Handle handles any requests sent to the identity provider's // endpoints. The URL.Path in the request will contain only the // handler local path, that is the part after URLPrefix above. // The given request will have had ParseForm called. Handle(ctx context.Context, w http.ResponseWriter, req *http.Request) // GetGroups retrieves additional group information that is // stored in the identity provider for the given identity. // TODO define what happens when the identity doesn't exist. GetGroups(ctx context.Context, id *store.Identity) (groups []string, err error) } golang-github-canonical-candid-1.12.3/idp/idptest/000077500000000000000000000000001457263123000217255ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/idptest/client.go000066400000000000000000000034141457263123000235340ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package idptest import ( "context" "crypto/sha256" "encoding/base64" "net/http" "net/http/httptest" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil" "github.com/canonical/candid/idp/idputil/secret" ) // A Client allows tests to simulate sending HTTP requests to an IDP. type Client struct { idp idp.IdentityProvider codec *secret.Codec loginState *http.Cookie state string } // NewClient create a new client for the given IDP. func NewClient(idp idp.IdentityProvider, codec *secret.Codec) *Client { return &Client{ idp: idp, codec: codec, } } // SetLoginStatus sets a login status that will be added to every // request. func (c *Client) SetLoginState(state idputil.LoginState) { value, err := c.codec.Encode(state) if err != nil { panic(err) } c.loginState = &http.Cookie{ Name: idputil.LoginCookieName, Value: value, } rawValue, err := base64.URLEncoding.DecodeString(value) if err != nil { panic(err) } hash := sha256.Sum256(rawValue) c.state = base64.RawURLEncoding.EncodeToString(hash[:]) } // Do simulates a round trip to the idp handler. func (c *Client) Do(req *http.Request) (*http.Response, error) { if c.loginState != nil { v := req.URL.Query() v.Set("state", c.state) req.URL.RawQuery = v.Encode() req.AddCookie(c.loginState) } req.ParseForm() w := httptest.NewRecorder() c.idp.Handle(context.Background(), w, req) return w.Result(), nil } // Get simulates a get request on the idp handler. func (c *Client) Get(url string) (*http.Response, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, errgo.Mask(err) } return c.Do(req) } golang-github-canonical-candid-1.12.3/idp/idptest/suite.go000066400000000000000000000245121457263123000234110ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package idptest import ( "bytes" "context" "crypto/sha256" "encoding/base64" "html/template" "io/ioutil" "net/http" "net/http/cookiejar" "net/http/httptest" "net/url" "strings" "time" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/qthttptest" "github.com/juju/simplekv" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil" "github.com/canonical/candid/idp/idputil/secret" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) // Fixture provides a test fixture that is helpful for testing identity // providers. type Fixture struct { // Ctx holds a context appropriate for using // for store methods. Ctx context.Context // Codec contains a codec that will be passed in the idp.InitParams. Codec *secret.Codec // Oven contains a bakery.Oven that will be passed in the // idp.InitParams. Tests can use this to mint macaroons if // necessary. Oven *bakery.Oven // Store holds the store used by the fixture. Store *candidtest.Store // Template holds the template to use for generating pages Template *template.Template dischargeTokenCreator *dischargeTokenCreator visitCompleter *visitCompleter kvStore simplekv.Store } func NewFixture(c *qt.C, store *candidtest.Store) *Fixture { ctx, closeStore := store.Store.Context(context.Background()) c.Cleanup(closeStore) ctx, closeMeetingStore := store.MeetingStore.Context(ctx) c.Cleanup(closeMeetingStore) key, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) oven := bakery.NewOven(bakery.OvenParams{ Key: key, Location: "idptest", }) kv, err := store.ProviderDataStore.KeyValueStore(ctx, "idptest") c.Assert(err, qt.IsNil) return &Fixture{ Ctx: ctx, Codec: secret.NewCodec(key), Oven: oven, Store: store, Template: candidtest.DefaultTemplate, dischargeTokenCreator: &dischargeTokenCreator{}, visitCompleter: &visitCompleter{ c: c, }, kvStore: kv, } } // InitParams returns a completed InitParams that a test can use to pass // to idp.Init. func (s *Fixture) InitParams(c *qt.C, prefix string) idp.InitParams { return idp.InitParams{ Store: s.Store.Store, KeyValueStore: s.kvStore, Oven: s.Oven, Codec: s.Codec, URLPrefix: prefix, DischargeTokenCreator: s.dischargeTokenCreator, VisitCompleter: s.visitCompleter, Template: s.Template, } } // LoginState creates a candid-login with the given login state. func (s *Fixture) LoginState(c *qt.C, state idputil.LoginState) (*http.Cookie, string) { value, err := s.Codec.Encode(state) c.Assert(err, qt.IsNil) rawValue, err := base64.URLEncoding.DecodeString(value) c.Assert(err, qt.IsNil) hash := sha256.Sum256(rawValue) return &http.Cookie{ Name: idputil.LoginCookieName, Value: value, }, base64.RawURLEncoding.EncodeToString(hash[:]) } // Client creates an HTTP client that will replace the given prefix with // the given replacement in all request URLs. The client will also stop // redirecting and return the last response when a request with the given // stopPrefix is attempted. func (s *Fixture) Client(c *qt.C, prefix, replacement, stopPrefix string) *http.Client { jar, err := cookiejar.New(nil) c.Assert(err, qt.IsNil) return &http.Client{ Transport: qthttptest.URLRewritingTransport{ MatchPrefix: prefix, Replace: replacement, RoundTripper: http.DefaultTransport, }, CheckRedirect: func(req *http.Request, via []*http.Request) error { if strings.HasPrefix(req.URL.String(), stopPrefix) { return http.ErrUseLastResponse } return nil }, Jar: jar, } } // ParseResponse parses a store.Identity from the given HTTP response. func (s *Fixture) ParseResponse(c *qt.C, resp *http.Response) (*store.Identity, error) { switch resp.StatusCode { case http.StatusOK: buf, err := ioutil.ReadAll(resp.Body) c.Assert(err, qt.IsNil) parts := bytes.Split(buf, []byte("\n")) if len(parts) > 1 && len(parts[1]) > 0 { return nil, errgo.New(string(parts[1])) } case http.StatusSeeOther: ru, err := url.Parse(resp.Header.Get("Location")) c.Assert(err, qt.IsNil) rv := ru.Query() if msg := rv.Get("error"); msg != "" { if code := rv.Get("error_code"); code != "" { return nil, errgo.WithCausef(nil, params.ErrorCode(code), "%s", msg) } return nil, errgo.New(msg) } c.Assert(rv.Get("code"), qt.Equals, "6789") return s.visitCompleter.id, nil default: c.Fatalf("unexpected response type: %s", resp.Status) } return nil, nil } // DoInteractiveLogin performs a full interactive login cycle with the // given IDP. func (s *Fixture) DoInteractiveLogin(c *qt.C, idp idp.IdentityProvider, loginURL string, f func(*http.Client, *http.Response) (*http.Response, error)) (*store.Identity, error) { u, err := url.Parse(loginURL) c.Assert(err, qt.IsNil) hu := *u hu.Path = "" srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { req.ParseForm() idp.Handle(req.Context(), w, req) })) defer srv.Close() client := s.Client(c, hu.String(), srv.URL, "http://result.example.com") cookie, state := s.LoginState(c, idputil.LoginState{ ReturnTo: "http://result.example.com/callback", State: "1234", Expires: time.Now().Add(10 * time.Minute), }) client.Jar.SetCookies(&hu, []*http.Cookie{cookie}) v := u.Query() v.Set("state", state) u.RawQuery = v.Encode() resp, err := client.Get(u.String()) c.Assert(err, qt.IsNil) if f != nil { resp, err = f(client, resp) c.Assert(err, qt.IsNil) } defer resp.Body.Close() return s.ParseResponse(c, resp) } // AssertLoginSuccess asserts that the login test has resulted in a // successful login of the given user. func (s *Fixture) AssertLoginSuccess(c *qt.C, username string) { c.Assert(s.visitCompleter.called, qt.Equals, true) c.Check(s.visitCompleter.err, qt.Equals, nil) c.Assert(s.visitCompleter.id, qt.Not(qt.IsNil)) c.Assert(s.visitCompleter.id.Username, qt.Equals, username) } // AssertLoginRedirectSuccess asserts that the given redirect URL is for // a successful login of the given user. func (s *Fixture) AssertLoginRedirectSuccess(c *qt.C, rurl, returnTo, state string, username string) { u, err := url.Parse(rurl) c.Assert(err, qt.IsNil) v := u.Query() u.RawQuery = "" c.Assert(u.String(), qt.Equals, returnTo) c.Assert(v.Get("state"), qt.Equals, state) c.Assert(v.Get("code"), qt.Equals, "6789") c.Assert(s.visitCompleter.id.Username, qt.Equals, username) } // AssertLoginFailureMatches asserts that the login test has resulted in a // failure with an error that matches the given regex. func (s *Fixture) AssertLoginFailureMatches(c *qt.C, regex string) { c.Assert(s.visitCompleter.called, qt.Equals, true) c.Assert(s.visitCompleter.err, qt.ErrorMatches, regex) } // AssertLoginRedirectFailureMatches asserts that the login test has resulted in a // failure with an error that matches the given regex. func (s *Fixture) AssertLoginRedirectFailureMatches(c *qt.C, rurl, returnTo, state, errorCode, regex string) { u, err := url.Parse(rurl) c.Assert(err, qt.IsNil) v := u.Query() u.RawQuery = "" c.Assert(u.String(), qt.Equals, returnTo) c.Assert(v.Get("state"), qt.Equals, state) c.Assert(v.Get("error_code"), qt.Equals, errorCode) c.Assert(v.Get("error"), qt.ErrorMatches, regex) } // AssertLoginNotComplete asserts that the login attempt has not yet // completed. func (s *Fixture) AssertLoginNotComplete(c *qt.C) { c.Assert(s.visitCompleter.called, qt.Equals, false) } type visitCompleter struct { c *qt.C called bool dischargeID string id *store.Identity err error } func (l *visitCompleter) Success(_ context.Context, _ http.ResponseWriter, _ *http.Request, dischargeID string, id *store.Identity) { if l.called { l.c.Error("login completion method called more than once") return } l.called = true l.dischargeID = dischargeID l.id = id } func (l *visitCompleter) Failure(_ context.Context, _ http.ResponseWriter, _ *http.Request, dischargeID string, err error) { if l.called { l.c.Error("login completion method called more than once") return } l.called = true l.dischargeID = dischargeID l.err = err } // RedirectSuccess implements isp.VisitCompleter.RedirectSuccess. func (l *visitCompleter) RedirectSuccess(_ context.Context, w http.ResponseWriter, req *http.Request, returnTo, state string, id *store.Identity) { if l.called { l.c.Error("login completion method called more than once") return } l.id = id u, err := url.Parse(returnTo) if err != nil { l.c.Error(err) return } v := u.Query() v.Set("state", state) v.Set("code", "6789") u.RawQuery = v.Encode() http.Redirect(w, req, u.String(), http.StatusSeeOther) } // RedirectFailure implements isp.VisitCompleter.RedirectFailure. func (l *visitCompleter) RedirectFailure(_ context.Context, w http.ResponseWriter, req *http.Request, returnTo, state string, verr error) { if l.called { l.c.Error("login completion method called more than once") return } l.called = true u, err := url.Parse(returnTo) if err != nil { l.c.Error(err) return } v := u.Query() v.Set("state", state) v.Set("error", verr.Error()) if ec, ok := errgo.Cause(verr).(errorCoder); ok { v.Set("error_code", string(ec.ErrorCode())) } u.RawQuery = v.Encode() http.Redirect(w, req, u.String(), http.StatusSeeOther) } func (l *visitCompleter) RedirectMFA(ctx context.Context, w http.ResponseWriter, req *http.Request, requireMFA bool, returnTo, returnToState, state string, id *store.Identity) { // NOTE: mfa is currently not tested via unit-tests. l.RedirectSuccess(ctx, w, req, returnTo, returnToState, id) } func (f *Fixture) Reset() { f.visitCompleter.called = false f.visitCompleter.dischargeID = "" f.visitCompleter.id = nil f.visitCompleter.err = nil } type errorCoder interface { ErrorCode() params.ErrorCode } type dischargeTokenCreator struct{} func (d *dischargeTokenCreator) DischargeToken(_ context.Context, id *store.Identity) (*httpbakery.DischargeToken, error) { return &httpbakery.DischargeToken{ Kind: "test", Value: []byte(id.Username), }, nil } golang-github-canonical-candid-1.12.3/idp/idputil/000077500000000000000000000000001457263123000217235ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/idputil/idputil.go000066400000000000000000000167171457263123000237400ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package idputil contains utility routines common to many identity // providers. package idputil import ( "context" "fmt" "html/template" "net/http" "net/url" "path" "time" "github.com/juju/loggo" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) var logger = loggo.GetLogger("candid.idp.idputil") var ReservedUsernames = map[string]bool{ "admin": true, "everyone": true, } // GetLoginMethods uses c to perform a request to get the list of // available login methods from u. The result is unmarshalled into v. func GetLoginMethods(ctx context.Context, c *httprequest.Client, u *url.URL, v interface{}) error { req, err := http.NewRequest("GET", u.String(), nil) if err != nil { return errgo.Mask(err) } req.Header.Set("Accept", "application/json") if err := c.Do(ctx, req, v); err != nil { return errgo.Mask(err) } return nil } // RequestParams creates an httprequest.Params object from the given fields. func RequestParams(ctx context.Context, w http.ResponseWriter, req *http.Request) httprequest.Params { return httprequest.Params{ Response: w, Request: req, Context: ctx, } } // DischargeID gets the discharge ID from the given request using the // standard form value. func DischargeID(req *http.Request) string { return req.Form.Get("id") } // URL creates a URL addressed to the given path within the IDP handler // and adds the given dischargeID (when specified). func URL(prefix, path, dischargeID string) string { callback := prefix + path v := make(url.Values) if dischargeID != "" { v.Set("id", dischargeID) } if len(v) > 0 { callback += "?" + v.Encode() } return callback } // State gets the state from the given request using the standard form // value. func State(req *http.Request) string { return req.Form.Get("state") } // RedirectURL creates a URL addressed to the given path within the IDP handler // and adds the given state. func RedirectURL(prefix, path, state string) string { v := url.Values{ "state": {state}, } return prefix + path + "?" + v.Encode() } type RegistrationParams struct { params.TemplateBrandParameters // State contains some opaque state for the registration. It can // be used to pass arbitrary data back to the idp once the // registration is processed. State string // Username contains the preferred username for the user. This // will be used to populate the username input. Username string // Error contains an error message if the registration failed. Error string // Domain contains the domain in which the user is being created. // This cannot be modified by the user. Domain string // FullName contains the full name of the user. This is used to // populate the fullname input. FullName string // Email contains the email address of the user. This is used to // populate the email input. Email string // Groups contains a CSV formatted list of groups the user is // a member of. This is used to populate the group input. Groups string } // RegistrationForm writes a registration form to the given writer using // the given parameters. func RegistrationForm(ctx context.Context, w http.ResponseWriter, args RegistrationParams, t *template.Template) error { t = t.Lookup("register") if t == nil { errgo.New("registration template not found") } w.Header().Set("Content-Type", "text/html;charset=utf-8") args.TemplateBrandParameters = params.BrandParameters() if err := t.Execute(w, args); err != nil { return errgo.Notef(err, "cannot process registration template") } return nil } // NameWithDomain builds a name out of name and domain. If domain is // empty then name is returned unchanged. func NameWithDomain(name, domain string) string { if domain == "" { return name } return name + "@" + domain } // LoginCookieName is the name of the cookie used to store LoginState // whilst a login is being processed. const LoginCookieName = "candid-login" // LoginCookiePath is the path to associate with the cookie storing the // current login state. const LoginCookiePath = "/login" // LoginState holds the state of the current loging process. type LoginState struct { // ReturnTo holds the address to return to after the login has // completed. ReturnTo string // State holds an opaque value from the original requesing server // that will be sent back to the ReturnTo URL when the login // attempt completes. State string // Expires holds the time that this login attempt should expire. Expires time.Time // ProvideID holds the ProviderID of an authenticated user. It is // only used when the user that has authenticaated requires // registration. ProviderID store.ProviderIdentity } // BadRequestf writes the given bad request message to the given // ResponseWriter. It should be used by IDPs when they do not have enough // state to pass the error message along to the initiating page. func BadRequestf(w http.ResponseWriter, f string, args ...interface{}) { w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, f, args...) } // LoginFormParams contains the parameters sent to the login-form // template. type LoginFormParams struct { params.IDPChoiceDetails params.TemplateBrandParameters // Action contains the action parameter for the form. Action string // Error contains an error message from the previous, failed, // login attempt. Error string } // HandleLoginForm is a handler that displays and process a standard login form. func HandleLoginForm( ctx context.Context, w http.ResponseWriter, req *http.Request, idpChoice params.IDPChoiceDetails, tmpl *template.Template, loginUser func(ctx context.Context, username, password string) (*store.Identity, error), ) (*store.Identity, error) { var errorMessage string switch req.Method { default: return nil, errgo.WithCausef(nil, params.ErrBadRequest, "unsupported method %q", req.Method) case "POST": id, err := loginUser(ctx, req.Form.Get("username"), req.Form.Get("password")) if err == nil { return id, nil } errorMessage = err.Error() case "GET": } data := LoginFormParams{ IDPChoiceDetails: idpChoice, TemplateBrandParameters: params.BrandParameters(), Action: idpChoice.URL, Error: errorMessage, } return nil, errgo.Mask(tmpl.ExecuteTemplate(w, "login-form", data)) } // ServiceURL determines the URL within the specified location. If the // given dest is a relative URL then a new url is calculated relative to // location, otherwise it is returned unchanged. func ServiceURL(location, dest string) string { if dest == "" { return "" } u, err := url.Parse(dest) if err != nil { // dest doesn't parse as a URL, assume the user knows // what they're doing and return if unchanged return dest } if u.Scheme != "" { // The dest URL is fully formed so don't modify it. return dest } lu, err := url.Parse(location) if err != nil { // The location doesn't parse as a URL, so we cannot be // realtive to it. Return the dest unchanged. return dest } lu.Path = path.Join(lu.Path, u.Path) return lu.String() } // CookiePathRelativeToLocation returns the Login Cookie Path // relative to the sub-path in the location URL given. // If skipLocation = true, then it's a no-op. func CookiePathRelativeToLocation(cookiePath, location string, skipLocation bool) string { if skipLocation { return cookiePath } u, err := url.Parse(location) if err != nil { return cookiePath } return u.Path + cookiePath } golang-github-canonical-candid-1.12.3/idp/idputil/msgraph/000077500000000000000000000000001457263123000233645ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/idputil/msgraph/msgraph.go000066400000000000000000000075641457263123000253700ustar00rootroot00000000000000// Copyright 2022 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package msgraph contains Microsoft Graph API utility routines // common to Azure and ADFS providers. package msgraph import ( "bytes" "context" "encoding/json" "io/ioutil" "net/http" "strings" "golang.org/x/oauth2" "gopkg.in/errgo.v1" ) // The Azure AD Graph API is deprecated in favor of the Microsoft Graph API, // however Azure AD still returns URLs that point to the depracated API. // Once the Azure AD Graph API is decomissioned, this replaced can be removed. // See https://docs.microsoft.com/en-us/graph/migrate-azure-ad-graph-request-differences#basic-requests var replacer = strings.NewReplacer( "https://graph.windows.net/", "https://graph.microsoft.com/v1.0/", "https://graph.microsoftazure.us/", "https://graph.microsoft.us/v1.0/", "https://graph.microsoftazure.us/", "https://dod-graph.microsoft.us/v1.0/", "https://graph.cloudapi.de/", "https://graph.microsoft.de/v1.0/", "https://graph.chinacloudapi.cn/", "https://microsoftgraph.chinacloudapi.cn/v1.0/") type MsGraphGroupsRetriever struct{} // This function handles Microsoft Graph API specifics around retrieving user groups. // If the user is a member of more than 150 groups (SAML) or 200 groups (JWT), groups must be explicitly // retrieved using the Microsoft Graph API. // See https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens func (msgr *MsGraphGroupsRetriever) RetrieveGroups(ctx context.Context, token *oauth2.Token, claimsUnmarshaler func(interface{}) error) ([]string, error) { var claims msGraphClaims err := claimsUnmarshaler(&claims) if err != nil { return nil, errgo.Newf("Failed to unmarshal claims.") } // Return a list of groups, if the claim is present. There is no need to query the Microsoft Graph API. if claims.Groups != nil { return claims.Groups, nil } var ok bool var claimName string if claimName, ok = claims.ClaimsNames["groups"]; !ok { return nil, nil } var claimSource claimSource if claimSource, ok = claims.ClaimsSources[claimName]; !ok { return nil, errgo.Newf("There is no '%s' item in the '_claim_sources' claim.", claimName) } reqBody := getMemberObjectsRequest{SecurityEnabledOnly: false} reqBodyJson, err := json.Marshal(reqBody) if err != nil { return nil, errgo.Notef(err, "Failed to marshal request body to JSON.") } url := replacer.Replace(claimSource.Endpoint) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(reqBodyJson)) if err != nil { return nil, errgo.Notef(err, "Failed to create a POST request.") } token.SetAuthHeader(req) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { return nil, errgo.Notef(err, "Groups request failed.") } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, errgo.Notef(err, "Failed to read response body.") } var mor getMemberObjectsResponse err = json.Unmarshal(body, &mor) if err != nil { return nil, errgo.Newf("Failed to unmarshal groups.") } return mor.Groups, nil } // Token claims need to get groups // See https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens type msGraphClaims struct { Groups []string `json:"groups"` ClaimsNames map[string]string `json:"_claim_names"` ClaimsSources map[string]claimSource `json:"_claim_sources"` } type claimSource struct { Endpoint string `json:"endpoint"` } // See https://docs.microsoft.com/en-us/graph/api/directoryobject-getmemberobjects?view=graph-rest-1.0&tabs=http#request-body type getMemberObjectsRequest struct { SecurityEnabledOnly bool `json:"securityEnabledOnly"` } // See https://docs.microsoft.com/en-us/graph/api/directoryobject-getmemberobjects?view=graph-rest-1.0&tabs=http#response-1 type getMemberObjectsResponse struct { Groups []string `json:"value"` } golang-github-canonical-candid-1.12.3/idp/idputil/secret/000077500000000000000000000000001457263123000232105ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/idputil/secret/codec.go000066400000000000000000000113471457263123000246220ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package secret import ( "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/json" "net/http" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "golang.org/x/crypto/nacl/box" "gopkg.in/errgo.v1" ) var ( ErrDecryption = errgo.New("decryption error") ErrInvalidCookie = errgo.New("invalid cookie") ) // Codec is used to create an encrypted messages that will be decrypted // by the same service. This should be used for cookies and other data // that needs to be sent through the client but must be verifiable as // originally coming from this service. type Codec struct { public, shared *[bakery.KeyLen]byte } // NewCodec creates a new Codec using the given key. func NewCodec(key *bakery.KeyPair) *Codec { shared := new([bakery.KeyLen]byte) box.Precompute(shared, (*[bakery.KeyLen]byte)(&key.Public.Key), (*[bakery.KeyLen]byte)(&key.Private.Key)) return &Codec{ public: (*[bakery.KeyLen]byte)(&key.Public.Key), shared: shared, } } // Encode marshals the given value in such a way that it can only be // unmarshaled by a Codec using the same key. The encoded output will be // in the base64 url safe alphabet. func (c *Codec) Encode(v interface{}) (string, error) { out, err := c.encode(v) if err != nil { return "", errgo.Mask(err) } return base64.URLEncoding.EncodeToString(out), nil } func (c *Codec) encode(v interface{}) ([]byte, error) { msg, err := json.Marshal(v) if err != nil { return nil, errgo.Mask(err) } out := make([]byte, 0, bakery.KeyLen+bakery.NonceLen+len(msg)+box.Overhead) out, err = c.encrypt(out, msg) if err != nil { return nil, errgo.Mask(err) } return out, nil } // encrypt encrypts the given message. func (c *Codec) encrypt(out, msg []byte) ([]byte, error) { var nonce [bakery.NonceLen]byte if _, err := rand.Read(nonce[:]); err != nil { return nil, errgo.Mask(err) } out = append(out, c.public[:]...) out = append(out, nonce[:]...) out = box.SealAfterPrecomputation(out, msg, &nonce, c.shared) return out, nil } // Decode unmarshals a value from the given buffer that must have been // marshaled with a Codec using the same key. If there was an error // decrypting buf the returned error will have a cause of ErrDecryption. func (c *Codec) Decode(s string, v interface{}) error { buf, err := base64.URLEncoding.DecodeString(s) if err != nil { return errgo.Mask(err) } return errgo.Mask(c.decode(buf, v), errgo.Is(ErrDecryption)) } func (c *Codec) decode(b []byte, v interface{}) error { if len(b) < bakery.KeyLen+bakery.NonceLen+box.Overhead { return errgo.New("buffer too short to decode") } out := make([]byte, 0, len(b)-bakery.KeyLen-bakery.NonceLen-box.Overhead) out, err := c.decrypt(out, b) if err != nil { return errgo.Mask(err, errgo.Is(ErrDecryption)) } return errgo.Mask(json.Unmarshal(out, v)) } // decrypt decrypts the message from the encrypted data. The given value // of must be long enough to contain at least the public key, nonce and // box.Overhead. func (c *Codec) decrypt(out, in []byte) ([]byte, error) { var public [bakery.KeyLen]byte var nonce [bakery.NonceLen]byte copy(public[:], in) if public != *c.public { return nil, errgo.WithCausef(nil, ErrDecryption, "unknown public key") } copy(nonce[:], in[len(public):]) out, ok := box.OpenAfterPrecomputation(out, in[len(public)+len(nonce):], &nonce, c.shared) if !ok { return nil, ErrDecryption } return out, nil } // SetCookie encodes the given value as a session cookie with the given // name. The returned value is used the verify the cookie later - it // should be passed to Cookie when the cookie is retrieved. func (c *Codec) SetCookie(w http.ResponseWriter, name, path string, v interface{}) (string, error) { out, err := c.encode(v) if err != nil { return "", errgo.Mask(err) } hash := sha256.Sum256(out) http.SetCookie(w, &http.Cookie{ Name: name, Value: base64.URLEncoding.EncodeToString(out), Path: path, }) return base64.RawURLEncoding.EncodeToString(hash[:]), nil } // Cookie decodes the cookie with the given name from the given request // into v. The given verification string is used to ensure the cookie is // valid. func (c *Codec) Cookie(req *http.Request, name, verification string, v interface{}) error { cookie, err := req.Cookie(name) if err != nil { return errgo.WithCausef(err, ErrInvalidCookie, "invalid cookie") } buf, err := base64.URLEncoding.DecodeString(cookie.Value) if err != nil { return errgo.WithCausef(err, ErrInvalidCookie, "invalid cookie") } hash := sha256.Sum256(buf) if base64.RawURLEncoding.EncodeToString(hash[:]) != verification { return errgo.WithCausef(nil, ErrInvalidCookie, "invalid cookie") } return errgo.Mask(c.decode(buf, v), errgo.Is(ErrDecryption)) } golang-github-canonical-candid-1.12.3/idp/idputil/secret/codec_test.go000066400000000000000000000123671457263123000256640ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package secret_test import ( "net/http" "net/http/httptest" "testing" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "gopkg.in/errgo.v1" "github.com/canonical/candid/idp/idputil/secret" ) var testKey = bakery.MustGenerateKey() func TestRoundTrip(t *testing.T) { c := qt.New(t) codec := secret.NewCodec(testKey) var a, b struct { A int B string } a.A = 1 a.B = "test" msg, err := codec.Encode(a) c.Assert(err, qt.IsNil) err = codec.Decode(msg, &b) c.Assert(err, qt.IsNil) c.Assert(b, qt.DeepEquals, a) } func TestDecodeBadBase64(t *testing.T) { c := qt.New(t) codec := secret.NewCodec(testKey) var a, b struct { A int B string } a.A = 1 a.B = "test" msg, err := codec.Encode(a) c.Assert(err, qt.IsNil) msg = "(" + msg[1:] err = codec.Decode(msg, &b) c.Assert(err, qt.ErrorMatches, "illegal base64 data at input byte 0") } func TestDecodeBadPublicKey(t *testing.T) { c := qt.New(t) codec := secret.NewCodec(testKey) var a, b struct { A int B string } a.A = 1 a.B = "test" msg, err := codec.Encode(a) c.Assert(err, qt.IsNil) msg = "A" + msg[:len(msg)-1] err = codec.Decode(msg, &b) c.Assert(err, qt.ErrorMatches, "unknown public key") c.Assert(errgo.Cause(err), qt.Equals, secret.ErrDecryption) } func TestDecodeDecryptionError(t *testing.T) { c := qt.New(t) codec := secret.NewCodec(testKey) var a, b struct { A int B string } a.A = 1 a.B = "test" msg, err := codec.Encode(a) c.Assert(err, qt.IsNil) msg = msg[:44] + msg err = codec.Decode(msg, &b) c.Assert(err, qt.ErrorMatches, "decryption error") c.Assert(errgo.Cause(err), qt.Equals, secret.ErrDecryption) } func TestDecodeBufferTooShort(t *testing.T) { c := qt.New(t) codec := secret.NewCodec(testKey) var a, b struct { A int B string } a.A = 1 a.B = "test" msg, err := codec.Encode(a) c.Assert(err, qt.IsNil) msg = msg[:40] err = codec.Decode(msg, &b) c.Assert(err, qt.ErrorMatches, "buffer too short to decode") } func TestDecodeUnmarshalError(t *testing.T) { c := qt.New(t) codec := secret.NewCodec(testKey) var a struct { A int B string } a.A = 1 a.B = "test" msg, err := codec.Encode(a) c.Assert(err, qt.IsNil) ej := errorJSON{errgo.New("test error")} err = codec.Decode(msg, &ej) c.Assert(err, qt.ErrorMatches, "test error") } func TestEncodeMarshalError(t *testing.T) { c := qt.New(t) codec := secret.NewCodec(testKey) msg, err := codec.Encode(errorJSON{errgo.New("test error")}) c.Assert(err, qt.ErrorMatches, "json: error calling MarshalJSON for type secret_test.errorJSON: test error") c.Assert(msg, qt.Equals, "") } type errorJSON struct { err error } func (e errorJSON) MarshalJSON() ([]byte, error) { return nil, e.err } func (e errorJSON) UnmarshalJSON([]byte) error { return e.err } func TestCookieRoundTrip(t *testing.T) { c := qt.New(t) codec := secret.NewCodec(testKey) w := httptest.NewRecorder() var a, b struct { A int B string } a.A = 1 a.B = "test" verification, err := codec.SetCookie(w, "test-cookie", "/", a) c.Assert(err, qt.IsNil) resp := w.Result() defer resp.Body.Close() cookies := resp.Cookies() c.Assert(cookies, qt.HasLen, 1) c.Assert(cookies[0].Name, qt.Equals, "test-cookie") req, err := http.NewRequest("", "", nil) c.Assert(err, qt.IsNil) req.AddCookie(cookies[0]) err = codec.Cookie(req, "test-cookie", verification, &b) c.Assert(err, qt.IsNil) c.Assert(b, qt.DeepEquals, a) } func TestCookieNoCookie(t *testing.T) { c := qt.New(t) codec := secret.NewCodec(testKey) req, err := http.NewRequest("", "", nil) c.Assert(err, qt.IsNil) err = codec.Cookie(req, "test-cookie", "1234", nil) c.Assert(err, qt.ErrorMatches, `invalid cookie: http: named cookie not present`) c.Assert(errgo.Cause(err), qt.Equals, secret.ErrInvalidCookie) } func TestCookieDecodeError(t *testing.T) { c := qt.New(t) codec := secret.NewCodec(testKey) w := httptest.NewRecorder() var a struct { A int B string } a.A = 1 a.B = "test" _, err := codec.SetCookie(w, "test-cookie", "/", a) c.Assert(err, qt.IsNil) resp := w.Result() defer resp.Body.Close() cookies := resp.Cookies() c.Assert(cookies, qt.HasLen, 1) c.Assert(cookies[0].Name, qt.Equals, "test-cookie") cookies[0].Value = "=" + cookies[0].Value req, err := http.NewRequest("", "", nil) c.Assert(err, qt.IsNil) req.AddCookie(cookies[0]) err = codec.Cookie(req, "test-cookie", "1234", nil) c.Assert(err, qt.ErrorMatches, `invalid cookie: illegal base64 data at input byte 0`) c.Assert(errgo.Cause(err), qt.Equals, secret.ErrInvalidCookie) } func TestCookieValidationError(t *testing.T) { c := qt.New(t) codec := secret.NewCodec(testKey) w := httptest.NewRecorder() var a struct { A int B string } a.A = 1 a.B = "test" _, err := codec.SetCookie(w, "test-cookie", "/", a) c.Assert(err, qt.IsNil) resp := w.Result() defer resp.Body.Close() cookies := resp.Cookies() c.Assert(cookies, qt.HasLen, 1) c.Assert(cookies[0].Name, qt.Equals, "test-cookie") req, err := http.NewRequest("", "", nil) c.Assert(err, qt.IsNil) req.AddCookie(cookies[0]) err = codec.Cookie(req, "test-cookie", "1234", nil) c.Assert(err, qt.ErrorMatches, `invalid cookie`) c.Assert(errgo.Cause(err), qt.Equals, secret.ErrInvalidCookie) } golang-github-canonical-candid-1.12.3/idp/keycloak/000077500000000000000000000000001457263123000220535ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/keycloak/keycloak.go000066400000000000000000000052421457263123000242070ustar00rootroot00000000000000// Copyright 2020 Mark Klein // Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package keycloak is an identity provider that authenticates with keycloak oidc. package keycloak import ( oidc "github.com/coreos/go-oidc" "gopkg.in/errgo.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/openid" ) const ( defaultProviderName = "keycloak" defaultProviderDomain = "KEYCLOAK" ) func init() { idp.Register("keycloak", func(unmarshal func(interface{}) error) (idp.IdentityProvider, error) { var p Params if err := unmarshal(&p); err != nil { return nil, errgo.Notef(err, "cannot unmarshal keycloak parameters") } if p.ClientID == "" { return nil, errgo.Newf("client-id not specified") } if p.KeycloakRealm == "" { return nil, errgo.Newf("keycloak-realm not specified") } return NewIdentityProvider(p), nil }) } // Params is a struct containing the configuration data to register a keycloak identity Provider type Params struct { // Name is the name that will be given to the identity provider. Name string `yaml:"name"` // Description is the description that will be used with the // identity provider. If this is not set then Name will be used. Description string `yaml:"description"` // Icon contains the URL or path of an icon. Icon string `yaml:"icon"` // Domain is the domain with which all identities created by this // identity provider will be tagged (not including the @ separator). Domain string `yaml:"domain"` // ClientID contains the Application Id for the application // registered ClientID string `yaml:"client-id"` // Optional: ClientSecret contains a password type Application Secret // for the application generated ClientSecret string `yaml:"client-secret"` // KeycloakReam contains the URI for the keycloak server // https:///auth/realms/ KeycloakRealm string `yaml:"keycloak-realm"` // Hidden is set if the IDP should be hidden from interactive // prompts. Hidden bool `yaml:"hidden"` } // NewIdentityProvider creates a keycloak identity provider with the // configuration defined by p. func NewIdentityProvider(p Params) idp.IdentityProvider { if p.Name == "" { p.Name = defaultProviderName } if p.Domain == "" { p.Domain = defaultProviderDomain } return openid.NewOpenIDConnectIdentityProvider(openid.OpenIDConnectParams{ Name: p.Name, Issuer: p.KeycloakRealm, Domain: p.Domain, Description: p.Description, Icon: p.Icon, Scopes: []string{oidc.ScopeOpenID, "profile"}, ClientID: p.ClientID, ClientSecret: p.ClientSecret, Hidden: p.Hidden, }) } golang-github-canonical-candid-1.12.3/idp/keycloak/keycloak_test.go000066400000000000000000000032121457263123000252410ustar00rootroot00000000000000// Copyright 2020 Mark Klein // Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package keycloak_test import ( "testing" qt "github.com/frankban/quicktest" "gopkg.in/yaml.v2" "github.com/canonical/candid/config" ) var configTests = []struct { about string yaml string expectError string }{{ about: "good config", yaml: ` identity-providers: - type: keycloak client-id: client-001 client-secret: secret-001 keycloak-realm: https://example.com/auth/realms/example `, }, { about: "another good config", yaml: ` identity-providers: - type: keycloak client-id: client-001 keycloak-realm: https://example.com/auth/realms/example `, }, { about: "no client-id", yaml: ` identity-providers: - type: keycloak client-secret: secret-001 keycloak-realm: https://example.com/auth/realms/example `, expectError: `cannot unmarshal keycloak configuration: client-id not specified`, }, { about: "no keycloak-realm", yaml: ` identity-providers: - type: keycloak client-id: client-001 client-secret: secret-001 `, expectError: `cannot unmarshal keycloak configuration: keycloak-realm not specified`, }} func TestConfig(t *testing.T) { c := qt.New(t) for _, test := range configTests { c.Run(test.about, func(c *qt.C) { var conf config.Config err := yaml.Unmarshal([]byte(test.yaml), &conf) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) return } c.Assert(err, qt.Equals, nil) c.Assert(conf.IdentityProviders, qt.HasLen, 1) c.Assert(conf.IdentityProviders[0].Name(), qt.Equals, "keycloak") }) } } golang-github-canonical-candid-1.12.3/idp/keystone/000077500000000000000000000000001457263123000221125ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/keystone/common_test.go000066400000000000000000000035431457263123000247750ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package keystone_test import ( qt "github.com/frankban/quicktest" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idptest" keystoneidp "github.com/canonical/candid/idp/keystone" "github.com/canonical/candid/idp/keystone/internal/keystone" "github.com/canonical/candid/idp/keystone/internal/mockkeystone" "github.com/canonical/candid/internal/candidtest" ) const idpPrefix = "https://idp.example.com" type fixture struct { idptest *idptest.Fixture server *mockkeystone.Server params keystoneidp.Params idp idp.IdentityProvider } type fixtureParams struct { newIDP func(p keystoneidp.Params) idp.IdentityProvider // The folllowing fields correspond with similarly named // fields in mockkeystone.Server, which will be initialized // with the values there. tokensFunc func(*keystone.TokensRequest) (*keystone.TokensResponse, error) authTokensFunc func(*keystone.AuthTokensRequest) (*keystone.AuthTokensResponse, error) tenantsFunc func(*keystone.TenantsRequest) (*keystone.TenantsResponse, error) userGroupsFunc func(*keystone.UserGroupsRequest) (*keystone.UserGroupsResponse, error) } func newFixture(c *qt.C, p fixtureParams) *fixture { s := &fixture{} candidtest.LogTo(c) s.idptest = idptest.NewFixture(c, candidtest.NewStore()) s.server = mockkeystone.NewServer() c.Defer(s.server.Close) s.params = keystoneidp.Params{ Name: "openstack", Description: "OpenStack", Domain: "openstack", URL: s.server.URL, } s.server.TokensFunc = p.tokensFunc s.server.AuthTokensFunc = p.authTokensFunc s.server.TenantsFunc = p.tenantsFunc s.server.UserGroupsFunc = p.userGroupsFunc s.idp = p.newIDP(s.params) err := s.idp.Init(s.idptest.Ctx, s.idptest.InitParams(c, idpPrefix)) c.Assert(err, qt.IsNil) return s } golang-github-canonical-candid-1.12.3/idp/keystone/discharge_test.go000066400000000000000000000076321457263123000254410ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package keystone_test import ( "context" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/form" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" envschemaform "gopkg.in/juju/environschema.v1/form" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/keystone" "github.com/canonical/candid/idp/keystone/internal/mockkeystone" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/internal/discharger" "github.com/canonical/candid/internal/identity" ) type dischargeSuite struct { candid *candidtest.Server dischargeCreator *candidtest.DischargeCreator server *mockkeystone.Server params keystone.Params } func TestDischarge(t *testing.T) { qtsuite.Run(qt.New(t), &dischargeSuite{}) } func (s *dischargeSuite) Init(c *qt.C) { candidtest.LogTo(c) s.server = mockkeystone.NewServer() c.Defer(s.server.Close) s.params = keystone.Params{ Name: "openstack", Description: "OpenStack", Domain: "openstack", URL: s.server.URL, } s.server.TokensFunc = testTokens s.server.TenantsFunc = testTenants store := candidtest.NewStore() sp := store.ServerParams() sp.IdentityProviders = []idp.IdentityProvider{ keystone.NewIdentityProvider(s.params), keystone.NewUserpassIdentityProvider( keystone.Params{ Name: "form", Domain: s.params.Domain, Description: s.params.Description, URL: s.params.URL, }, ), keystone.NewTokenIdentityProvider( keystone.Params{ Name: "token", Domain: s.params.Domain, Description: s.params.Description, URL: s.params.URL, }, ), } s.candid = candidtest.NewServer(c, sp, map[string]identity.NewAPIHandlerFunc{ "discharger": discharger.NewAPIHandler, }) s.dischargeCreator = candidtest.NewDischargeCreator(s.candid) } func (s *dischargeSuite) TestInteractiveDischarge(c *qt.C) { s.dischargeCreator.AssertDischarge(c, httpbakery.WebBrowserInteractor{ OpenWebBrowser: candidtest.PasswordLogin(c, "testuser", "testpass"), }) } func (s *dischargeSuite) TestFormDischarge(c *qt.C) { s.dischargeCreator.AssertDischarge(c, form.Interactor{ Filler: keystoneFormFiller{ username: "testuser", password: "testpass", }, }) } type keystoneFormFiller struct { username, password string } func (h keystoneFormFiller) Fill(f envschemaform.Form) (map[string]interface{}, error) { if _, ok := f.Fields["username"]; !ok { return nil, errgo.New("schema has no username") } if _, ok := f.Fields["password"]; !ok { return nil, errgo.New("schema has no password") } return map[string]interface{}{ "username": h.username, "password": h.password, }, nil } func (s *dischargeSuite) TestTokenDischarge(c *qt.C) { s.dischargeCreator.AssertDischarge(c, &tokenInteractor{}) } type tokenLoginRequest struct { httprequest.Route `httprequest:"POST"` Token keystone.Token `httprequest:",body"` } type TokenInteractionInfo struct { URL string `json:"url"` } type tokenInteractor struct{} func (i *tokenInteractor) Kind() string { return "token" } func (i *tokenInteractor) Interact(ctx context.Context, client *httpbakery.Client, location string, ierr *httpbakery.Error) (*httpbakery.DischargeToken, error) { var info keystone.TokenInteractionInfo if err := ierr.InteractionMethod("token", &info); err != nil { return nil, errgo.Mask(err, errgo.Is(httpbakery.ErrInteractionMethodNotFound)) } var req keystone.TokenLoginRequest req.Token.Login.ID = "789" var resp keystone.TokenLoginResponse cl := &httprequest.Client{ Doer: client, } if err := cl.CallURL(ctx, info.URL, &req, &resp); err != nil { return nil, errgo.Mask(err) } return resp.DischargeToken, nil } golang-github-canonical-candid-1.12.3/idp/keystone/export_test.go000066400000000000000000000002441457263123000250210ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package keystone var KeystoneSchemaResponse = keystoneSchemaResponse golang-github-canonical-candid-1.12.3/idp/keystone/internal/000077500000000000000000000000001457263123000237265ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/keystone/internal/keystone/000077500000000000000000000000001457263123000255675ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/keystone/internal/keystone/client.go000066400000000000000000000070431457263123000274000ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package keystone import ( "context" "net/http" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" ) const subjectTokenHeader = "X-Subject-Token" // Client provides access to a keystone server. Currently the supported // protocols are versions 2.0 & 3, see // http://developer.openstack.org/api-ref-identity-v2.html or // http://developer.openstack.org/api-ref/identity/v3/index.html for more // information. type Client struct { client httprequest.Client } // NewClient creates a new Client for the keystone server at url. func NewClient(url string) *Client { return &Client{ client: httprequest.Client{ BaseURL: url, UnmarshalError: unmarshalError, }, } } // Tokens provides access to the /v2.0/tokens endpoint. See // http://developer.openstack.org/api-ref-identity-v2.html#authenticate-v2.0 // for more information. func (c *Client) Tokens(ctx context.Context, r *TokensRequest) (*TokensResponse, error) { var resp TokensResponse if err := c.client.Call(ctx, r, &resp); err != nil { return nil, err } return &resp, nil } // Tenants provides access to the /v2.0/tenants endpoint. See // http://developer.openstack.org/api-ref-identity-v2.html#listTenants // for more information. func (c *Client) Tenants(ctx context.Context, r *TenantsRequest) (*TenantsResponse, error) { var resp TenantsResponse if err := c.client.Call(ctx, r, &resp); err != nil { return nil, err } return &resp, nil } // AuthTokens provides access to the /v3/auth/tokens endpoint. See // http://developer.openstack.org/api-ref/identity/v3/index.html?expanded=password-authentication-with-unscoped-authorization-detail // for more information. This uses version 3 of the keystone protocol and // therefore cannot be used with older keystone servers that don't // support it. func (c *Client) AuthTokens(ctx context.Context, r *AuthTokensRequest) (*AuthTokensResponse, error) { // Initially get the whole http.Response so that we can read the // "X-Subject-Token" header. var resp *http.Response if err := c.client.Call(ctx, r, &resp); err != nil { return nil, err } var authResp AuthTokensResponse if err := httprequest.UnmarshalJSONResponse(resp, &authResp); err != nil { return nil, err } authResp.SubjectToken = resp.Header.Get(subjectTokenHeader) return &authResp, nil } // UserGroups provides access to the /v3/users/:id/groups endpoint. See // http://developer.openstack.org/api-ref/identity/v3/index.html?expanded=list-groups-to-which-a-user-belongs-detail // for more information. This uses version 3 of the keystone protocol and // therefore cannot be used with older keystone servers that don't // support it. func (c *Client) UserGroups(ctx context.Context, r *UserGroupsRequest) (*UserGroupsResponse, error) { var resp UserGroupsResponse if err := c.client.Call(ctx, r, &resp); err != nil { return nil, err } return &resp, nil } // Error represents an error from a keystone server. type Error struct { Code int `json:"code"` Title string `json:"title"` Message string `json:"message"` } func (e *Error) Error() string { return e.Message } // ErrorResponse represents an error response from the keystone server. type ErrorResponse struct { Error *Error `json:"error"` } func unmarshalError(r *http.Response) error { var jerr ErrorResponse if err := httprequest.UnmarshalJSONResponse(r, &jerr); err != nil { return err } if jerr.Error == nil || jerr.Error.Message == "" { return errgo.Newf("unsupported error response: %s", r.Status) } return jerr.Error } golang-github-canonical-candid-1.12.3/idp/keystone/internal/keystone/client_test.go000066400000000000000000000026151457263123000304370ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package keystone_test import ( "io/ioutil" "net/http" "net/url" "strings" "testing" qt "github.com/frankban/quicktest" "github.com/canonical/candid/idp/keystone/internal/keystone" ) var unmarshalErrorTests = []struct { about string body string expect *keystone.Error expectError string }{{ about: "error", body: `{"error":{"code":400, "message":"test error","title":"Test"}}`, expect: &keystone.Error{ Code: 400, Message: "test error", Title: "Test", }, expectError: "test error", }, { about: "bad json", body: `{"error":{"code":400, "message":"test error","title":"Test"}`, expectError: "unexpected end of JSON input", }} func TestUnmarshalError(t *testing.T) { c := qt.New(t) for _, test := range unmarshalErrorTests { c.Run(test.about, func(c *qt.C) { resp := &http.Response{ Body: ioutil.NopCloser(strings.NewReader(test.body)), Header: http.Header{ "Content-Type": {"application/json"}, }, Request: &http.Request{ Method: "GET", URL: &url.URL{ Scheme: "http", Host: "example.com", Path: "test", }, }, } err := keystone.UnmarshalError(resp) if test.expect != nil { c.Assert(err, qt.DeepEquals, test.expect) } c.Assert(err, qt.ErrorMatches, test.expectError) }) } } golang-github-canonical-candid-1.12.3/idp/keystone/internal/keystone/export_test.go000066400000000000000000000002241457263123000304740ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package keystone var UnmarshalError = unmarshalError golang-github-canonical-candid-1.12.3/idp/keystone/internal/keystone/params.go000066400000000000000000000142021457263123000274000ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package keystone implements a keystone client. package keystone import ( "net/http" "time" "gopkg.in/httprequest.v1" ) // TokensRequest is the request sent to /v2.0/tokens to perform a login. // See // http://developer.openstack.org/api-ref-identity-v2.html#authenticate-v2.0 // for more information. type TokensRequest struct { httprequest.Route `httprequest:"POST /v2.0/tokens"` Body TokensBody `httprequest:",body"` } // TokensBody represents the JSON body sent in a login request. type TokensBody struct { Auth Auth `json:"auth"` } // TokensResponse is the response from /v2.0/tokens on success. type TokensResponse struct { Access Access `json:"access"` } // Auth is the authentication information sent in a login request. type Auth struct { TenantName string `json:"tenantName,omitempty"` TenantID string `json:"tenantId,omitempty"` PasswordCredentials *PasswordCredentials `json:"passwordCredentials,omitempty"` Token *Token `json:"token,omitempty"` } // PasswordCredentials holds the credentials for a username/password // authentication. type PasswordCredentials struct { Username string `json:"username"` Password string `json:"password"` } // Token contains the details of a token generated by keystone. type Token struct { ID string `json:"id,omitempty"` IssuedAt *Time `json:"issued_at,omitempty"` Expires *Time `json:"expires,omitempty"` Tenant *Tenant `json:"tenant,omitempty"` } // Tenant contains details of a tenant in the openstack environment. type Tenant struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Enabled bool `json:"enabled"` } // Access contains the access granted in the login attempt. type Access struct { Token Token `json:"token"` User User `json:"user"` } // User contains details of a user in the openstack environment. type User struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` Username string `json:"username,omitemtpy"` Domain *Domain `json:"domain,omitempty"` Password string `json:"password,omitempty"` } // TenantsRequest is the request sent to /v2.0/tenants to list tenants a // token has access to. See // http://developer.openstack.org/api-ref-identity-v2.html#listTenants // for more information. type TenantsRequest struct { httprequest.Route `httprequest:"GET /v2.0/tenants"` AuthToken string `httprequest:"X-Auth-Token,header"` } // TenantsResponse is the list of tenants a token has access to. type TenantsResponse struct { Tenants []Tenant `json:"tenants"` } // Time is a time.Time that provides a custom UnmarshalJSON method. type Time struct { time.Time } func (t *Time) UnmarshalJSON(data []byte) error { if err := t.Time.UnmarshalJSON(data); err == nil { return nil } var err error t.Time, err = time.Parse(`"2006-01-02T15:04:05"`, string(data)) return err } // AuthTokensRequest is the request sent to /v3/auth/tokens to perform a // login. See // http://developer.openstack.org/api-ref/identity/v3/index.html?expanded=password-authentication-with-unscoped-authorization-detail // for more information. type AuthTokensRequest struct { httprequest.Route `httprequest:"POST /v3/auth/tokens"` Body AuthTokensBody `httprequest:",body"` } // AuthTokensBody represents the JSON body sent in a v3 login request. type AuthTokensBody struct { Auth AuthV3 `json:"auth"` } // AuthV3 is the authentication information sent in a v3 login request. type AuthV3 struct { Identity Identity `json:"identity"` } // Identity contains the identity information sent in a v3 login request. type Identity struct { Methods []string `json:"methods"` Password *Password `json:"password,omitempty"` Token *IdentityToken `json:"token,omitempty"` } // Password contains the password based identity information sent in a // v3 login request. type Password struct { User User `json:"user"` } // IdentityToken contains the token based identity information sent in a // v3 login request. type IdentityToken struct { ID string `json:"id"` } // Domain contains the domain of a user in the v3 API. type Domain struct { ID string `json:"id,omitempty"` Name string `json:"name:omitempty"` } // AuthTokensResponse is the reponse sent by /v3/auth/tokens when there // has been a successful login. See // http://developer.openstack.org/api-ref/identity/v3/index.html?expanded=password-authentication-with-unscoped-authorization-detail // for more information. type AuthTokensResponse struct { SubjectToken string Token TokenV3 `json:"token"` } // SetHeader implements httprequest.HeaderSetter by setting the // appropriate X-Subject-Token header for the response. func (resp AuthTokensResponse) SetHeader(h http.Header) { h.Set(subjectTokenHeader, resp.SubjectToken) } // TokenV3 represents the token returned from /v3/auth/tokens after a // successful login. type TokenV3 struct { IssuedAt *Time `json:"issued_at,omitempty"` Methods []string `json:"methods,omitempty"` ExpiresAt *Time `json:"expires_at,omitempty"` User User `json:"user"` } // UserGroupsRequest represents a request to the /v3/users/:id/groups // endpoint. See // http://developer.openstack.org/api-ref/identity/v3/index.html?expanded=list-groups-to-which-a-user-belongs-detail // for more information. type UserGroupsRequest struct { httprequest.Route `httprequest:"GET /v3/users/:UserID/groups"` UserID string `httprequest:",path"` AuthToken string `httprequest:"X-Auth-Token,header"` } // UserGroupsResponse represents a response to the /v3/users/:id/groups // endpoint. See // http://developer.openstack.org/api-ref/identity/v3/index.html?expanded=list-groups-to-which-a-user-belongs-detail // for more information. type UserGroupsResponse struct { Groups []Group `json:"groups"` } // Group contains information on a keystone group. type Group struct { ID string `json:"id"` DomainID string `json:"domain_id"` Name string `json:"name"` Description string `json:"description"` } golang-github-canonical-candid-1.12.3/idp/keystone/internal/keystone/params_test.go000066400000000000000000000021551457263123000304430ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package keystone_test import ( "fmt" "testing" "time" qt "github.com/frankban/quicktest" "github.com/canonical/candid/idp/keystone/internal/keystone" ) var timeUnmarshalJSONTests = []struct { json string expect time.Time expectError string }{{ json: `"2015-09-21T10:38:15.788236"`, expect: time.Date(2015, 9, 21, 10, 38, 15, 788236000, time.UTC), }, { json: `"2015-09-22T10:38:15Z"`, expect: time.Date(2015, 9, 22, 10, 38, 15, 0, time.UTC), }, { json: `"yesterday"`, expectError: `parsing time.*cannot parse.*as "2006"`, }} func TestTimeUnmarshalJSON(t *testing.T) { c := qt.New(t) for i, test := range timeUnmarshalJSONTests { c.Run(fmt.Sprintf("test%d", i), func(c *qt.C) { var t keystone.Time err := t.UnmarshalJSON([]byte(test.json)) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) return } c.Assert(err, qt.IsNil) c.Assert(t.Equal(test.expect), qt.Equals, true, qt.Commentf("obtained: %#v, expected: %#v", t, test.expect)) }) } } golang-github-canonical-candid-1.12.3/idp/keystone/internal/mockkeystone/000077500000000000000000000000001457263123000264415ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/keystone/internal/mockkeystone/server.go000066400000000000000000000055751457263123000303120ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package mockkeystone import ( "context" "net/http" "net/http/httptest" "github.com/julienschmidt/httprouter" "gopkg.in/httprequest.v1" "github.com/canonical/candid/idp/keystone/internal/keystone" ) // Server provides a mock keystone server for use in tests. type Server struct { *httptest.Server // TokensFunc handles the /v2.0/tokens endpoint. This must be set // before the endpoint can be used. TokensFunc func(*keystone.TokensRequest) (*keystone.TokensResponse, error) // AuthTokensFunc handles the /v3/auth/tokens endpoint. This must // be set before the endpoint can be used. AuthTokensFunc func(*keystone.AuthTokensRequest) (*keystone.AuthTokensResponse, error) // TenantsFunc handles the /v2.0/tenants endpoint. This must be set // before the endpoint can be used. TenantsFunc func(*keystone.TenantsRequest) (*keystone.TenantsResponse, error) // UserGroupsFunc handles the /v3/users/:id/groups endpoint. This must be set // before the endpoint can be used. UserGroupsFunc func(*keystone.UserGroupsRequest) (*keystone.UserGroupsResponse, error) } // NewServer creates a new Server for use in tests. func NewServer() *Server { s := new(Server) router := httprouter.New() for _, h := range reqServer.Handlers(s.handler) { router.Handle(h.Method, h.Path, h.Handle) } s.Server = httptest.NewServer(router) return s } // handler creates a new handler for a request. func (s *Server) handler(p httprequest.Params) (*handler, context.Context, error) { return &handler{ tokens: s.TokensFunc, authTokens: s.AuthTokensFunc, tenants: s.TenantsFunc, userGroups: s.UserGroupsFunc, }, p.Context, nil } var reqServer = httprequest.Server{ ErrorMapper: func(ctx context.Context, err error) (int, interface{}) { var resp keystone.ErrorResponse if kerr, ok := err.(*keystone.Error); ok { resp.Error = kerr } else { resp.Error = &keystone.Error{ Code: http.StatusInternalServerError, Message: err.Error(), } } return resp.Error.Code, &resp }, } type handler struct { tokens func(*keystone.TokensRequest) (*keystone.TokensResponse, error) authTokens func(*keystone.AuthTokensRequest) (*keystone.AuthTokensResponse, error) tenants func(*keystone.TenantsRequest) (*keystone.TenantsResponse, error) userGroups func(*keystone.UserGroupsRequest) (*keystone.UserGroupsResponse, error) } func (h *handler) Tokens(r *keystone.TokensRequest) (*keystone.TokensResponse, error) { return h.tokens(r) } func (h *handler) AuthTokens(r *keystone.AuthTokensRequest) (*keystone.AuthTokensResponse, error) { return h.authTokens(r) } func (h *handler) Tenants(r *keystone.TenantsRequest) (*keystone.TenantsResponse, error) { return h.tenants(r) } func (h *handler) UserGroups(r *keystone.UserGroupsRequest) (*keystone.UserGroupsResponse, error) { return h.userGroups(r) } golang-github-canonical-candid-1.12.3/idp/keystone/keystone.go000066400000000000000000000223261457263123000243070ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package keystone contains identity providers that validate against // keystone servers. package keystone import ( "context" "net/http" "strings" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/loggo" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil" "github.com/canonical/candid/idp/keystone/internal/keystone" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) var logger = loggo.GetLogger("candid.idp.keystone") func init() { idp.Register("keystone", constructor(NewIdentityProvider)) } // constructor returns a function that is suitable for passing to // config.RegisterIDP that will decode and validate Params from YAML. If // valid an identity provider will be created by calling f. func constructor(f func(Params) idp.IdentityProvider) func(func(interface{}) error) (idp.IdentityProvider, error) { return func(unmarshal func(interface{}) error) (idp.IdentityProvider, error) { var p Params if err := unmarshal(&p); err != nil { return nil, errgo.Notef(err, "cannot unmarshal keystone parameters") } if p.Name == "" { return nil, errgo.Newf("name not specified") } if p.URL == "" { return nil, errgo.Newf("url not specified") } return f(p), nil } } // Params holds the parameters to use with keystone identity providers. type Params struct { // Name is the name that the identity provider will have within // the identity manager. The name is used as part of the url for // communicating with the identity provider. Name string `yaml:"name"` // If Domain is set it will be appended to any usernames or // groups provided by the identity provider. A user created by // this identity provide would be username@domain. Domain string `yaml:"domain"` // Description is a human readable description that will be used // if a list of providers is shown for a user to choose. Description string `yaml:"description"` // Icon contains the URL or path of an icon. Icon string `yaml:"icon"` // URL is the address of the keystone server. URL string `yaml:"url"` // Hidden is set if the IDP should be hidden from interactive // prompts. Hidden bool `yaml:"hidden"` } // NewIdentityProvider creates an interactive keystone identity provider // with the configuration defined by p. func NewIdentityProvider(p Params) idp.IdentityProvider { idp := newIdentityProvider(p) return &idp } // newIdentityProvider creates an identityProvider with the configuration // defined by p. func newIdentityProvider(p Params) identityProvider { if p.Description == "" { p.Description = p.Name } if p.Icon == "" { p.Icon = "/static/images/icons/keystone.svg" } return identityProvider{ params: p, client: keystone.NewClient(p.URL), } } // identityProvider is an idp.IdentityProvider that authenticates against // a keystone server. type identityProvider struct { params Params initParams idp.InitParams client *keystone.Client } // Name implements idp.IdentityProvider.Name. func (idp *identityProvider) Name() string { return idp.params.Name } // Domain implements idp.IdentityProvider.Domain. func (idp *identityProvider) Domain() string { return idp.params.Domain } // Description implements idp.IdentityProvider.Description. func (idp *identityProvider) Description() string { return idp.params.Description } // IconURL returns the URL of an icon for the identity provider. func (idp *identityProvider) IconURL() string { return idputil.ServiceURL(idp.initParams.Location, idp.params.Icon) } // Interactive implements idp.IdentityProvider.Interactive. func (*identityProvider) Interactive() bool { return true } // Hidden implements idp.IdentityProvider.Hidden. func (idp *identityProvider) Hidden() bool { return idp.params.Hidden } // Init implements idp.IdentityProvider.Init. func (idp *identityProvider) Init(_ context.Context, params idp.InitParams) error { idp.initParams = params return nil } // URL implements idp.IdentityProvider.URL. func (idp *identityProvider) URL(state string) string { return idputil.RedirectURL(idp.initParams.URLPrefix, "/login", state) } // SetInteraction implements idp.IdentityProvider.SetInteraction. func (idp *identityProvider) SetInteraction(ierr *httpbakery.Error, dischargeID string) { } // GetGroups implements idp.IdentityProvider.GetGroups. func (*identityProvider) GetGroups(ctx context.Context, identity *store.Identity) ([]string, error) { // TODO(mhilton) store the token in the identity ProviderInfo and // retrieve groups on demand rather than on login. return identity.ProviderInfo["groups"], nil } // Handle implements idp.IdentityProvider.Handle. func (idp *identityProvider) Handle(ctx context.Context, w http.ResponseWriter, req *http.Request) { var ls idputil.LoginState if err := idp.initParams.Codec.Cookie(req, idputil.LoginCookieName, req.Form.Get("state"), &ls); err != nil { logger.Infof("Invalid login state: %s", err) idputil.BadRequestf(w, "Login failed: invalid login state") return } switch strings.TrimPrefix(req.URL.Path, idp.initParams.URLPrefix) { case "/login": idpChoice := params.IDPChoiceDetails{ Domain: idp.params.Domain, Description: idp.params.Description, Name: idp.params.Name, URL: idp.URL(req.Form.Get("state")), } id, err := idputil.HandleLoginForm(ctx, w, req, idpChoice, idp.initParams.Template, idp.loginUser) if err != nil { idp.initParams.VisitCompleter.RedirectFailure(ctx, w, req, ls.ReturnTo, ls.State, err) } if id != nil { idp.initParams.VisitCompleter.RedirectSuccess(ctx, w, req, ls.ReturnTo, ls.State, id) } } } func (idp *identityProvider) loginUser(ctx context.Context, username, password string) (*store.Identity, error) { return idp.doLogin(ctx, keystone.Auth{ PasswordCredentials: &keystone.PasswordCredentials{ Username: username, Password: password, }, }) } // doLogin performs the login with the keystone server. func (idp *identityProvider) doLogin(ctx context.Context, a keystone.Auth) (*store.Identity, error) { resp, err := idp.client.Tokens(ctx, &keystone.TokensRequest{ Body: keystone.TokensBody{ Auth: a, }, }) if err != nil { return nil, errgo.WithCausef(err, params.ErrUnauthorized, "cannot log in") } groups, err := idp.getGroups(ctx, resp.Access.Token.ID) if err != nil { return nil, errgo.Mask(err) } user := &store.Identity{ ProviderID: store.MakeProviderIdentity(idp.Name(), idp.qualifiedName(resp.Access.User.ID)), Username: idp.qualifiedName(resp.Access.User.Username), ProviderInfo: map[string][]string{ "groups": groups, }, } if err := idp.initParams.Store.UpdateIdentity( ctx, user, store.Update{ store.Username: store.Set, store.ProviderInfo: store.Set, }, ); err != nil { return nil, errgo.Notef(err, "cannot update identity") } return user, nil } // getGroups connects to keystone using token and lists tenants // associated with the token. The tenants are then converted to groups // names by suffixing with the domain, if configured. func (idp *identityProvider) getGroups(ctx context.Context, token string) ([]string, error) { resp, err := idp.client.Tenants(ctx, &keystone.TenantsRequest{ AuthToken: token, }) if err != nil { return nil, errgo.Notef(err, "cannot get tenants") } groups := make([]string, len(resp.Tenants)) for i, t := range resp.Tenants { groups[i] = t.Name } return groups, nil } // doLoginV3 performs the login with the keystone (version 3) server. func (idp *identityProvider) doLoginV3(ctx context.Context, a keystone.AuthV3) (*store.Identity, error) { resp, err := idp.client.AuthTokens(ctx, &keystone.AuthTokensRequest{ Body: keystone.AuthTokensBody{ Auth: a, }, }) if err != nil { return nil, errgo.WithCausef(err, params.ErrUnauthorized, "cannot log in") } groups, err := idp.getGroupsV3(ctx, resp.SubjectToken, resp.Token.User.ID) if err != nil { return nil, errgo.Mask(err) } user := &store.Identity{ ProviderID: store.MakeProviderIdentity(idp.Name(), idp.qualifiedName(resp.Token.User.ID)), Username: idp.qualifiedName(resp.Token.User.Name), ProviderInfo: map[string][]string{ "groups": groups, }, } if err := idp.initParams.Store.UpdateIdentity( ctx, user, store.Update{ store.Username: store.Set, store.ProviderInfo: store.Set, }, ); err != nil { return nil, errgo.Notef(err, "cannot update identity") } return user, nil } // getGroupsV3 connects to keystone using token and lists groups // associated with the user. The group names are suffixing with the // domain, if configured. func (idp *identityProvider) getGroupsV3(ctx context.Context, token, user string) ([]string, error) { resp, err := idp.client.UserGroups(ctx, &keystone.UserGroupsRequest{ AuthToken: token, UserID: user, }) if err != nil { return nil, errgo.Notef(err, "cannot get groups") } groups := make([]string, len(resp.Groups)) for i, g := range resp.Groups { groups[i] = g.Name } return groups, nil } // qualifiedName returns the given name qualified as appropriate with // the provider's configured domain. func (idp *identityProvider) qualifiedName(name string) string { if idp.params.Domain != "" { return name + "@" + idp.params.Domain } return name } golang-github-canonical-candid-1.12.3/idp/keystone/keystone_test.go000066400000000000000000000206201457263123000253410ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package keystone_test import ( "net/http" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" yaml "gopkg.in/yaml.v2" "github.com/canonical/candid/config" keystoneidp "github.com/canonical/candid/idp/keystone" "github.com/canonical/candid/idp/keystone/internal/keystone" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/store" ) type keystoneSuite struct { *fixture } func TestKeystone(t *testing.T) { qtsuite.Run(qt.New(t), &keystoneSuite{}) } func (s *keystoneSuite) Init(c *qt.C) { s.fixture = newFixture(c, fixtureParams{ newIDP: keystoneidp.NewIdentityProvider, tokensFunc: testTokens, tenantsFunc: testTenants, }) } func (s *keystoneSuite) TestKeystoneIdentityProviderName(c *qt.C) { c.Assert(s.idp.Name(), qt.Equals, "openstack") } func (s *keystoneSuite) TestKeystoneIdentityProviderDescription(c *qt.C) { c.Assert(s.idp.Description(), qt.Equals, "OpenStack") } func (s *keystoneSuite) TestIconURL(c *qt.C) { idp := keystoneidp.NewIdentityProvider(keystoneidp.Params{}) params := s.idptest.InitParams(c, idpPrefix) params.Location = "https://www.example.com/candid" err := idp.Init(s.idptest.Ctx, params) c.Assert(err, qt.IsNil) c.Assert(idp.IconURL(), qt.Equals, "https://www.example.com/candid/static/images/icons/keystone.svg") } func (s *keystoneSuite) TestAbsoluteIconURL(c *qt.C) { idp := keystoneidp.NewIdentityProvider(keystoneidp.Params{ Icon: "https://www.example.com/icon.bmp", }) err := idp.Init(s.idptest.Ctx, s.idptest.InitParams(c, idpPrefix)) c.Assert(err, qt.IsNil) c.Assert(idp.IconURL(), qt.Equals, "https://www.example.com/icon.bmp") } func (s *keystoneSuite) TestRelativeIconURL(c *qt.C) { idp := keystoneidp.NewIdentityProvider(keystoneidp.Params{ Icon: "/static/icon.bmp", }) params := s.idptest.InitParams(c, idpPrefix) params.Location = "https://www.example.com/candid" err := idp.Init(s.idptest.Ctx, params) c.Assert(err, qt.IsNil) c.Assert(idp.IconURL(), qt.Equals, "https://www.example.com/candid/static/icon.bmp") } func (s *keystoneSuite) TestKeystoneIdentityProviderInteractive(c *qt.C) { c.Assert(s.idp.Interactive(), qt.Equals, true) } func (s *keystoneSuite) TestKeystoneIdentityProviderHidden(c *qt.C) { c.Assert(s.idp.Hidden(), qt.Equals, false) p := s.params p.Hidden = true idp := keystoneidp.NewIdentityProvider(p) c.Assert(idp.Hidden(), qt.Equals, true) } func (s *keystoneSuite) TestKeystoneIdentityProviderUseNameForDescription(c *qt.C) { p := s.params p.Description = "" idp := keystoneidp.NewIdentityProvider(p) c.Assert(idp.Description(), qt.Equals, "openstack") } func (s *keystoneSuite) TestKeystoneIdentityProviderURL(c *qt.C) { u := s.idp.URL("1") c.Assert(u, qt.Equals, idpPrefix+"/login?state=1") } func (s *keystoneSuite) TestKeystoneIdentityProviderHandleSuccess(c *qt.C) { id, err := s.idptest.DoInteractiveLogin(c, s.idp, idpPrefix+"/login", candidtest.PostLoginForm("testuser", "testpass")) c.Assert(err, qt.IsNil) candidtest.AssertEqualIdentity(c, id, &store.Identity{ ProviderID: store.MakeProviderIdentity("openstack", "abc@openstack"), Username: "testuser@openstack", ProviderInfo: map[string][]string{ "groups": {"abc_project"}, }, }) s.idptest.Store.AssertUser(c, &store.Identity{ ProviderID: store.MakeProviderIdentity("openstack", "abc@openstack"), Username: "testuser@openstack", ProviderInfo: map[string][]string{ "groups": {"abc_project"}, }, }) } func (s *keystoneSuite) TestKeystoneIdentityProviderHandlePostBadPassword(c *qt.C) { _, err := s.idptest.DoInteractiveLogin(c, s.idp, idpPrefix+"/login", candidtest.PostLoginForm("testuser", "nottestpass")) c.Assert(err, qt.ErrorMatches, `cannot log in: Post http.*: invalid credentials`) } func (s *keystoneSuite) TestKeystoneIdentityProviderHandlePostNoTenants(c *qt.C) { _, err := s.idptest.DoInteractiveLogin(c, s.idp, idpPrefix+"/login", candidtest.PostLoginForm("testuser2", "testpass")) c.Assert(err, qt.ErrorMatches, `cannot get tenants: Get .*: bad token`) } func (s *keystoneSuite) TestKeystoneIdentityProviderHandleExistingUser(c *qt.C) { err := s.idptest.Store.Store.UpdateIdentity( s.idptest.Ctx, &store.Identity{ ProviderID: store.MakeProviderIdentity("keystone2", "testuser@openstack"), Username: "testuser@openstack", }, store.Update{ store.Username: store.Set, }, ) c.Assert(err, qt.IsNil) _, err = s.idptest.DoInteractiveLogin(c, s.idp, idpPrefix+"/login", candidtest.PostLoginForm("testuser", "testpass")) c.Assert(err, qt.ErrorMatches, `cannot update identity: username testuser@openstack already in use`) } var configTests = []struct { about string yaml string expectError string }{{ about: "good config", yaml: ` identity-providers: - type: keystone name: openstack url: https://example.com/keystone `, }, { about: "no name", yaml: ` identity-providers: - type: keystone url: https://example.com/keystone `, expectError: `cannot unmarshal keystone configuration: name not specified`, }, { about: "no url", yaml: ` identity-providers: - type: keystone name: openstack `, expectError: `cannot unmarshal keystone configuration: url not specified`, }} func (s *keystoneSuite) TestKeystoneIdentityProviderRegisterConfig(c *qt.C) { for _, test := range configTests { c.Run(test.about, func(c *qt.C) { var conf config.Config err := yaml.Unmarshal([]byte(test.yaml), &conf) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) return } c.Assert(err, qt.IsNil) c.Assert(conf.IdentityProviders, qt.HasLen, 1) c.Assert(conf.IdentityProviders[0].Name(), qt.Equals, "openstack") }) } } func testTokens(r *keystone.TokensRequest) (*keystone.TokensResponse, error) { var id string var username string if r.Body.Auth.PasswordCredentials != nil { switch r.Body.Auth.PasswordCredentials.Username { case "testuser": id = "123" case "testuser2": id = "456" default: return nil, &keystone.Error{ Code: http.StatusUnauthorized, Message: "invalid credentials", Title: "Unauthorized", } } if r.Body.Auth.PasswordCredentials.Password != "testpass" { return nil, &keystone.Error{ Code: http.StatusUnauthorized, Message: "invalid credentials", Title: "Unauthorized", } } username = r.Body.Auth.PasswordCredentials.Username } else { if r.Body.Auth.Token.ID != "789" { return nil, &keystone.Error{ Code: http.StatusUnauthorized, Message: "invalid credentials", Title: "Unauthorized", } } id = "123" username = "testuser" } return &keystone.TokensResponse{ Access: keystone.Access{ Token: keystone.Token{ ID: id, }, User: keystone.User{ ID: "abc", Username: username, Name: "Test User", }, }, }, nil } func testTenants(r *keystone.TenantsRequest) (*keystone.TenantsResponse, error) { if r.AuthToken != "123" { return nil, &keystone.Error{ Code: http.StatusUnauthorized, Message: "bad token", Title: "Unauthorized", } } return &keystone.TenantsResponse{ Tenants: []keystone.Tenant{{ ID: "def", Name: "abc_project", }}, }, nil } func testAuthTokens(req *keystone.AuthTokensRequest) (*keystone.AuthTokensResponse, error) { var id string var username string if req.Body.Auth.Identity.Password != nil { return nil, &keystone.Error{ Code: http.StatusUnauthorized, Message: "password authentication not yet supported.", Title: "Not Authorized", } } else { if req.Body.Auth.Identity.Token.ID != "789" { return nil, &keystone.Error{ Code: http.StatusUnauthorized, Message: "The request you have made requires authentication.", Title: "Not Authorized", } } id = "123" username = "testuser" } return &keystone.AuthTokensResponse{ SubjectToken: "abcd", Token: keystone.TokenV3{ User: keystone.User{ ID: id, Name: username, Domain: &keystone.Domain{ ID: "default", Name: "Default", }, }, }, }, nil } func testUserGroups(req *keystone.UserGroupsRequest) (*keystone.UserGroupsResponse, error) { if req.AuthToken != "abcd" { return nil, &keystone.Error{ Code: http.StatusUnauthorized, Message: "bad token", Title: "Unauthorized", } } return &keystone.UserGroupsResponse{ Groups: []keystone.Group{{ ID: "def", Name: "abc_group", DomainID: "default", }}, }, nil } golang-github-canonical-candid-1.12.3/idp/keystone/token.go000066400000000000000000000065421457263123000235700ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package keystone import ( "context" "net/http" "strings" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil" "github.com/canonical/candid/idp/keystone/internal/keystone" "github.com/canonical/candid/params" ) func init() { idp.Register("keystone_token", constructor(NewTokenIdentityProvider)) } // NewTokenIdentityProvider creates a idp.IdentityProvider which will // authenticate against a keystone server using existing tokens. func NewTokenIdentityProvider(p Params) idp.IdentityProvider { return &tokenIdentityProvider{ identityProvider: newIdentityProvider(p), } } // tokenIdentityProvider is an identity provider that uses a configured // keystone instance to authenticate against using an existing token to // authenticate. type tokenIdentityProvider struct { identityProvider } // Interactive implements idp.IdentityProvider.Interactive. func (*tokenIdentityProvider) Interactive() bool { return false } // SetInteraction implements idp.IdentityProvider.SetInteraction. func (idp *tokenIdentityProvider) SetInteraction(ierr *httpbakery.Error, dischargeID string) { ierr.SetInteraction("token", TokenInteractionInfo{ URL: idputil.URL(idp.initParams.URLPrefix, "/interact", dischargeID), }) } // Handle implements idp.IdentityProvider.Handle. func (idp *tokenIdentityProvider) Handle(ctx context.Context, w http.ResponseWriter, req *http.Request) { var lr TokenLoginRequest if err := httprequest.Unmarshal(idputil.RequestParams(ctx, w, req), &lr); err != nil { idp.initParams.VisitCompleter.Failure(ctx, w, req, idputil.DischargeID(req), errgo.WithCausef(err, params.ErrBadRequest, "cannot unmarshal login request")) return } user, err := idp.doLogin(ctx, keystone.Auth{ Token: &keystone.Token{ ID: lr.Token.Login.ID, }, }) if err != nil { idp.initParams.VisitCompleter.Failure(ctx, w, req, idputil.DischargeID(req), err) return } if strings.TrimPrefix(req.URL.Path, idp.initParams.URLPrefix) == "/interact" { dt, err := idp.initParams.DischargeTokenCreator.DischargeToken(ctx, user) if err != nil { idp.initParams.VisitCompleter.Failure(ctx, w, req, idputil.DischargeID(req), err) return } httprequest.WriteJSON(w, http.StatusOK, TokenLoginResponse{ DischargeToken: dt, }) } else { idp.initParams.VisitCompleter.Success(ctx, w, req, idputil.DischargeID(req), user) } } // TokenLoginRequest is the request sent for a token login. type TokenLoginRequest struct { httprequest.Route `httprequest:"POST"` Token Token `httprequest:",body"` } // TokenLoginResponse is the response sent for a token login. type TokenLoginResponse struct { DischargeToken *httpbakery.DischargeToken `json:"discharge-token"` } type idName struct { Name string `json:"name"` ID string `json:"id"` } // Token is the token sent to use to login to the keystone // server. The only part that is used is Login.ID. type Token struct { Login struct { Domain idName `json:"domain"` User idName `json:"user"` Tenant idName `json:"tenant"` ID string `json:"id"` } `json:"login"` } // TokenInteractionInfo is the interaction info for a token interactor. type TokenInteractionInfo struct { URL string `json:"url"` } golang-github-canonical-candid-1.12.3/idp/keystone/token_test.go000066400000000000000000000060171457263123000246240ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package keystone_test import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "gopkg.in/yaml.v2" "github.com/canonical/candid/config" keystoneidp "github.com/canonical/candid/idp/keystone" "github.com/canonical/candid/store" ) type tokenSuite struct { *fixture } func TestToken(t *testing.T) { qtsuite.Run(qt.New(t), &tokenSuite{}) } func (s *tokenSuite) Init(c *qt.C) { s.fixture = newFixture(c, fixtureParams{ newIDP: keystoneidp.NewTokenIdentityProvider, tokensFunc: testTokens, tenantsFunc: testTenants, }) } func (s *tokenSuite) TestKeystoneTokenIdentityProviderInteractive(c *qt.C) { c.Assert(s.idp.Interactive(), qt.Equals, false) } func (s *tokenSuite) TestKeystoneTokenIdentityProviderHidden(c *qt.C) { c.Assert(s.idp.Hidden(), qt.Equals, false) } func (s *tokenSuite) TestKeystoneTokenIdentityProviderHandle(c *qt.C) { var tok keystoneidp.Token tok.Login.ID = "789" body, err := json.Marshal(tok) c.Assert(err, qt.IsNil) req, err := http.NewRequest("POST", "https://idp.test/login?did=1", bytes.NewReader(body)) c.Assert(err, qt.IsNil) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() s.idp.Handle(s.idptest.Ctx, rr, req) s.idptest.AssertLoginSuccess(c, "testuser@openstack") s.idptest.Store.AssertUser(c, &store.Identity{ ProviderID: store.MakeProviderIdentity("openstack", "abc@openstack"), Username: "testuser@openstack", ProviderInfo: map[string][]string{ "groups": {"abc_project"}, }, }) } func (s *tokenSuite) TestKeystoneTokenIdentityProviderHandleBadToken(c *qt.C) { var tok keystoneidp.Token tok.Login.ID = "012" body, err := json.Marshal(tok) c.Assert(err, qt.IsNil) req, err := http.NewRequest("POST", "https://idp.test/login?did=1", bytes.NewReader(body)) c.Assert(err, qt.IsNil) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() s.idp.Handle(s.idptest.Ctx, rr, req) s.idptest.AssertLoginFailureMatches(c, `cannot log in: Post http.*: invalid credentials`) } func (s *tokenSuite) TestKeystoneTokenIdentityProviderHandleBadRequest(c *qt.C) { req, err := http.NewRequest("POST", "https://idp.test/login?did=1", strings.NewReader("{")) c.Assert(err, qt.IsNil) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() s.idp.Handle(s.idptest.Ctx, rr, req) s.idptest.AssertLoginFailureMatches(c, `cannot unmarshal login request: cannot unmarshal into field Token: cannot unmarshal request body: unexpected end of JSON input`) } func (s *tokenSuite) TestRegisterConfig(c *qt.C) { input := ` identity-providers: - type: keystone_token name: openstack3 url: https://example.com/keystone ` var conf config.Config err := yaml.Unmarshal([]byte(input), &conf) c.Assert(err, qt.IsNil) c.Assert(conf.IdentityProviders, qt.HasLen, 1) c.Assert(conf.IdentityProviders[0].Name(), qt.Equals, "openstack3") } golang-github-canonical-candid-1.12.3/idp/keystone/tokenv3.go000066400000000000000000000044421457263123000240360ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package keystone import ( "context" "net/http" "strings" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil" "github.com/canonical/candid/idp/keystone/internal/keystone" "github.com/canonical/candid/params" ) func init() { idp.Register("keystonev3_token", constructor(NewV3TokenIdentityProvider)) } // NewV3TokenIdentityProvider creates a idp.IdentityProvider which will // authenticate against a keystone (version 3) server using existing // tokens. func NewV3TokenIdentityProvider(p Params) idp.IdentityProvider { return &v3tokenIdentityProvider{ identityProvider: newIdentityProvider(p), } } // v3tokenIdentityProvider is an identity provider that uses a configured // keystone instance to authenticate against using an existing token to // authenticate. type v3tokenIdentityProvider struct { identityProvider } // Interactive implements idp.IdentityProvider.Interactive. func (*v3tokenIdentityProvider) Interactive() bool { return false } // Handle implements idp.IdentityProvider.Handle. func (idp *v3tokenIdentityProvider) Handle(ctx context.Context, w http.ResponseWriter, req *http.Request) { var lr TokenLoginRequest if err := httprequest.Unmarshal(idputil.RequestParams(ctx, w, req), &lr); err != nil { idp.initParams.VisitCompleter.Failure(ctx, w, req, idputil.DischargeID(req), errgo.WithCausef(err, params.ErrBadRequest, "cannot unmarshal login request")) return } user, err := idp.doLoginV3(ctx, keystone.AuthV3{ Identity: keystone.Identity{ Methods: []string{"token"}, Token: &keystone.IdentityToken{ ID: lr.Token.Login.ID, }, }, }) if err != nil { idp.initParams.VisitCompleter.Failure(ctx, w, req, idputil.DischargeID(req), err) return } if strings.TrimPrefix(req.URL.Path, idp.initParams.URLPrefix) == "/interact" { dt, err := idp.initParams.DischargeTokenCreator.DischargeToken(ctx, user) if err != nil { idp.initParams.VisitCompleter.Failure(ctx, w, req, idputil.DischargeID(req), err) return } httprequest.WriteJSON(w, http.StatusOK, TokenLoginResponse{ DischargeToken: dt, }) } else { idp.initParams.VisitCompleter.Success(ctx, w, req, idputil.DischargeID(req), user) } } golang-github-canonical-candid-1.12.3/idp/keystone/tokenv3_test.go000066400000000000000000000062211457263123000250720ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package keystone_test import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "gopkg.in/yaml.v2" "github.com/canonical/candid/config" keystoneidp "github.com/canonical/candid/idp/keystone" "github.com/canonical/candid/store" ) func TestTokenV3(t *testing.T) { qtsuite.Run(qt.New(t), &tokenV3Suite{}) } type tokenV3Suite struct { *fixture } func (s *tokenV3Suite) Init(c *qt.C) { s.fixture = newFixture(c, fixtureParams{ newIDP: keystoneidp.NewV3TokenIdentityProvider, authTokensFunc: testAuthTokens, userGroupsFunc: testUserGroups, tokensFunc: testTokens, tenantsFunc: testTenants, }) } func (s *tokenV3Suite) TestKeystoneV3TokenIdentityProviderInteractive(c *qt.C) { c.Assert(s.idp.Interactive(), qt.Equals, false) } func (s *tokenV3Suite) TestKeystoneV3TokenIdentityProviderHidden(c *qt.C) { c.Assert(s.idp.Hidden(), qt.Equals, false) } func (s *tokenV3Suite) TestKeystoneV3TokenIdentityProviderHandle(c *qt.C) { var tok keystoneidp.Token tok.Login.ID = "789" body, err := json.Marshal(tok) c.Assert(err, qt.IsNil) req, err := http.NewRequest("POST", "/login?did=1", bytes.NewReader(body)) c.Assert(err, qt.IsNil) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() s.idp.Handle(s.idptest.Ctx, rr, req) s.idptest.AssertLoginSuccess(c, "testuser@openstack") s.idptest.Store.AssertUser(c, &store.Identity{ ProviderID: store.MakeProviderIdentity("openstack", "123@openstack"), Username: "testuser@openstack", ProviderInfo: map[string][]string{ "groups": {"abc_group"}, }, }) } func (s *tokenV3Suite) TestKeystoneV3TokenIdentityProviderHandleBadToken(c *qt.C) { var tok keystoneidp.Token tok.Login.ID = "012" body, err := json.Marshal(tok) c.Assert(err, qt.IsNil) req, err := http.NewRequest("POST", "https://idp.test/login?did=1", bytes.NewReader(body)) c.Assert(err, qt.IsNil) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() s.idp.Handle(s.idptest.Ctx, rr, req) s.idptest.AssertLoginFailureMatches(c, `cannot log in: Post http.*: The request you have made requires authentication.`) } func (s *tokenV3Suite) TestKeystoneV3TokenIdentityProviderHandleBadRequest(c *qt.C) { req, err := http.NewRequest("POST", "https://idp.test/login?did=1", strings.NewReader("{")) c.Assert(err, qt.IsNil) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() s.idp.Handle(s.idptest.Ctx, rr, req) s.idptest.AssertLoginFailureMatches(c, `cannot unmarshal login request: cannot unmarshal into field Token: cannot unmarshal request body: unexpected end of JSON input`) } func (s *tokenV3Suite) TestRegisterConfig(c *qt.C) { input := ` identity-providers: - type: keystonev3_token name: openstackv3_3 url: https://example.com/keystone ` var conf config.Config err := yaml.Unmarshal([]byte(input), &conf) c.Assert(err, qt.IsNil) c.Assert(conf.IdentityProviders, qt.HasLen, 1) c.Assert(conf.IdentityProviders[0].Name(), qt.Equals, "openstackv3_3") } golang-github-canonical-candid-1.12.3/idp/keystone/userpass.go000066400000000000000000000076671457263123000243260ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package keystone import ( "context" "net/http" "strings" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/form" "github.com/juju/schema" "gopkg.in/errgo.v1" gooseidentity "gopkg.in/goose.v1/identity" "gopkg.in/httprequest.v1" "gopkg.in/juju/environschema.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil" "github.com/canonical/candid/idp/keystone/internal/keystone" "github.com/canonical/candid/params" ) func init() { idp.Register("keystone_userpass", constructor(NewUserpassIdentityProvider)) } // NewTokenIdentityProvider creates a idp.IdentityProvider which will // authenticate against a keystone server using a httpbakery.form // compatible login method. func NewUserpassIdentityProvider(p Params) idp.IdentityProvider { return &userpassIdentityProvider{ identityProvider: newIdentityProvider(p), } } // userpassIdentityProvider is an identity provider that uses a // configured keystone instance to authenticate against using // httpbakery.form to pass login parameters. type userpassIdentityProvider struct { identityProvider } // Interactive implements idp.IdentityProvider.Interactive. func (*userpassIdentityProvider) Interactive() bool { return false } // SetInteraction implements idp.IdentityProvider.SetInteraction. func (idp *userpassIdentityProvider) SetInteraction(ierr *httpbakery.Error, dischargeID string) { ierr.SetInteraction(form.InteractionMethod, form.InteractionInfo{ URL: idputil.URL(idp.initParams.URLPrefix, "/interact", dischargeID), }) } // Handle implements idp.IdentityProvider.Handle. func (idp *userpassIdentityProvider) Handle(ctx context.Context, w http.ResponseWriter, req *http.Request) { if req.Method != "POST" { httprequest.WriteJSON(w, http.StatusOK, keystoneSchemaResponse) return } var lr form.LoginRequest if err := httprequest.Unmarshal(idputil.RequestParams(ctx, w, req), &lr); err != nil { idp.initParams.VisitCompleter.Failure(ctx, w, req, idputil.DischargeID(req), errgo.WithCausef(err, params.ErrBadRequest, "cannot unmarshal login request")) return } frm, err := keystoneFieldsChecker.Coerce(lr.Body.Form, nil) if err != nil { idp.initParams.VisitCompleter.Failure(ctx, w, req, idputil.DischargeID(req), errgo.Notef(err, "cannot validate form")) return } m := frm.(map[string]interface{}) user, err := idp.doLogin(ctx, keystone.Auth{ PasswordCredentials: &keystone.PasswordCredentials{ Username: m["username"].(string), Password: m["password"].(string), }, }) if err != nil { idp.initParams.VisitCompleter.Failure(ctx, w, req, idputil.DischargeID(req), errgo.Notef(err, "cannot validate form")) return } if strings.TrimPrefix(req.URL.Path, idp.initParams.URLPrefix) == "/interact" { dt, err := idp.initParams.DischargeTokenCreator.DischargeToken(ctx, user) if err != nil { idp.initParams.VisitCompleter.Failure(ctx, w, req, idputil.DischargeID(req), err) return } httprequest.WriteJSON(w, http.StatusOK, form.LoginResponse{ Token: dt, }) } else { idp.initParams.VisitCompleter.Success(ctx, w, req, idputil.DischargeID(req), user) } } var keystoneSchemaResponse = form.SchemaResponse{ Schema: keystoneFields, } var keystoneFields = environschema.Fields{ "username": environschema.Attr{ Description: "username", Type: environschema.Tstring, Mandatory: true, EnvVars: gooseidentity.CredEnvUser, }, "password": environschema.Attr{ Description: "password", Type: environschema.Tstring, Mandatory: true, Secret: true, EnvVars: gooseidentity.CredEnvSecrets, }, } var keystoneFieldsChecker = schema.FieldMap(mustValidationSchema(keystoneFields)) func mustValidationSchema(fields environschema.Fields) (schema.Fields, schema.Defaults) { f, d, err := fields.ValidationSchema() if err != nil { panic(err) } return f, d } golang-github-canonical-candid-1.12.3/idp/keystone/userpass_test.go000066400000000000000000000074311457263123000253520ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package keystone_test import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/form" "github.com/juju/qthttptest" "gopkg.in/yaml.v2" "github.com/canonical/candid/config" keystoneidp "github.com/canonical/candid/idp/keystone" "github.com/canonical/candid/store" ) func TestUserPass(t *testing.T) { qtsuite.Run(qt.New(t), &userpassSuite{}) } type userpassSuite struct { *fixture } func (s *userpassSuite) Init(c *qt.C) { s.fixture = newFixture(c, fixtureParams{ newIDP: keystoneidp.NewUserpassIdentityProvider, tokensFunc: testTokens, tenantsFunc: testTenants, }) } func (s *userpassSuite) TestKeystoneUserpassIdentityProviderInteractive(c *qt.C) { c.Assert(s.idp.Interactive(), qt.Equals, false) } func (s *userpassSuite) TestKeystoneUserpassIdentityProviderHidden(c *qt.C) { c.Assert(s.idp.Hidden(), qt.Equals, false) } func (s *userpassSuite) TestKeystoneUserpassIdentityProviderHandle(c *qt.C) { req, err := http.NewRequest("GET", "https://idp.test/login?did=1", nil) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() s.idp.Handle(s.idptest.Ctx, rr, req) s.idptest.AssertLoginNotComplete(c) qthttptest.AssertJSONResponse(c, rr, http.StatusOK, keystoneidp.KeystoneSchemaResponse) } func (s *userpassSuite) TestKeystoneUserpassIdentityProviderHandleResponse(c *qt.C) { login := map[string]interface{}{ "username": "testuser", "password": "testpass", } body, err := json.Marshal(form.LoginBody{ Form: login, }) c.Assert(err, qt.IsNil) req, err := http.NewRequest("POST", "/login?did=1", bytes.NewReader(body)) c.Assert(err, qt.IsNil) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() s.idp.Handle(s.idptest.Ctx, rr, req) s.idptest.AssertLoginSuccess(c, "testuser@openstack") identity := s.idptest.Store.AssertUser(c, &store.Identity{ ProviderID: store.MakeProviderIdentity("openstack", "abc@openstack"), Username: "testuser@openstack", ProviderInfo: map[string][]string{ "groups": {"abc_project"}, }, }) groups, err := s.idp.GetGroups(s.idptest.Ctx, identity) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"abc_project"}) } func (s *userpassSuite) TestKeystoneUserpassIdentityProviderHandleBadRequest(c *qt.C) { req, err := http.NewRequest("POST", "/login?did=1", strings.NewReader("{")) c.Assert(err, qt.IsNil) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() s.idp.Handle(s.idptest.Ctx, rr, req) s.idptest.AssertLoginFailureMatches(c, `cannot unmarshal login request: cannot unmarshal into field Body: cannot unmarshal request body: unexpected end of JSON input`) } func (s *userpassSuite) TestKeystoneUserpassIdentityProviderHandleNoUsername(c *qt.C) { login := map[string]interface{}{ "password": "testpass", } body, err := json.Marshal(form.LoginBody{ Form: login, }) c.Assert(err, qt.IsNil) req, err := http.NewRequest("POST", "https://idp.test/login?did=1", bytes.NewReader(body)) c.Assert(err, qt.IsNil) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() s.idp.Handle(s.idptest.Ctx, rr, req) s.idptest.AssertLoginFailureMatches(c, `cannot validate form: username: expected string, got nothing`) } func (s *userpassSuite) TestRegisterConfig(c *qt.C) { input := ` identity-providers: - type: keystone_userpass name: openstack2 url: https://example.com/keystone ` var conf config.Config err := yaml.Unmarshal([]byte(input), &conf) c.Assert(err, qt.IsNil) c.Assert(conf.IdentityProviders, qt.HasLen, 1) c.Assert(conf.IdentityProviders[0].Name(), qt.Equals, "openstack2") } golang-github-canonical-candid-1.12.3/idp/ldap/000077500000000000000000000000001457263123000211715ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/ldap/discharge_test.go000066400000000000000000000021651457263123000245140ustar00rootroot00000000000000// Copyright 2019 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package ldap_test import ( "testing" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/ldap" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/internal/discharger" "github.com/canonical/candid/internal/identity" ) func TestInteractiveDischarge(t *testing.T) { c := qt.New(t) defer c.Done() store := candidtest.NewStore() sp := store.ServerParams() ldapIDP, err := ldap.NewIdentityProvider(getSampleParams()) c.Assert(err, qt.IsNil) ldap.SetLDAP(ldapIDP, newMockLDAPDialer(getSampleLdapDB()).Dial) sp.IdentityProviders = []idp.IdentityProvider{ ldapIDP, } candid := candidtest.NewServer(c, sp, map[string]identity.NewAPIHandlerFunc{ "discharger": discharger.NewAPIHandler, }) dischargeCreator := candidtest.NewDischargeCreator(candid) dischargeCreator.AssertDischarge(c, httpbakery.WebBrowserInteractor{ OpenWebBrowser: candidtest.PasswordLogin(c, "user1", "pass1"), }) } golang-github-canonical-candid-1.12.3/idp/ldap/export_test.go000066400000000000000000000006341457263123000241030ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package ldap import ( "github.com/canonical/candid/idp" ) type LDAPConn ldapConn type LDAPDialer func(network, address string) (LDAPConn, error) func SetLDAP(p idp.IdentityProvider, dialer LDAPDialer) { p.(*identityProvider).dialLDAP = func(netw, addr string) (ldapConn, error) { return dialer(netw, addr) } } golang-github-canonical-candid-1.12.3/idp/ldap/ldap.go000066400000000000000000000343241457263123000224460ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package ldap contains identity providers that validate against ldap // servers. package ldap import ( "bytes" "context" "crypto/tls" "crypto/x509" "fmt" "net" "net/http" "net/url" "strings" "text/template" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/loggo" "gopkg.in/errgo.v1" "gopkg.in/ldap.v2" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) var logger = loggo.GetLogger("candid.idp.ldap") func init() { idp.Register("ldap", func(unmarshal func(interface{}) error) (idp.IdentityProvider, error) { var p Params if err := unmarshal(&p); err != nil { return nil, errgo.Notef(err, "cannot unmarshal ldap parameters") } if p.Name == "" { return nil, errgo.Newf("name not specified") } idp, err := NewIdentityProvider(p) if err != nil { return nil, errgo.Mask(err) } return idp, nil }) } type Params struct { // Name is the name that will be given to the identity provider. Name string `yaml:"name"` // Description is the description that will be used with the // identity provider. If this is not set then Name will be used. Description string `yaml:"description"` // Icon contains the URL or path of an icon. Icon string `yaml:"icon"` // Domain is the domain with which all identities created by this // identity provider will be tagged (not including the @ separator). Domain string `yaml:"domain"` // URL contains an LDAP URL indicating the server to connect to. URL string `yaml:"url"` // CACertificate contains a PEM encoded CA certificate to verify // the ldap connection against. CACertificate string `yaml:"ca-cert"` // DN contains the distinguished name that is used to bind to the // LDAP server to perform searches. If this is empty then the IDP // will bind anonymously and Password will be ignored. DN string `yaml:"dn"` // Password contains the password to use to when binding to the // LDAP server as DN. Password string `yaml:"password"` // RequireMFA indicates if this provider requires the use of MFA RequireMFA bool `yaml:"require-mfa"` // UserQueryFilter defines the filter for searching users. UserQueryFilter string `yaml:"user-query-filter"` // UserQueryAttrs defines how user attributes are mapped to attributes in // the LDAP entry. UserQueryAttrs UserQueryAttrs `yaml:"user-query-attrs"` // GroupQueryFilter defines the template for the LDAP filter to search for // the groups that a user belongs to. The .User value is defined to hold // the user id being searched for - e.g. // (&(objectClass=groupOfNames)(member={{.User}})) GroupQueryFilter string `yaml:"group-query-filter"` // Hidden is set if the IDP should be hidden from interactive // prompts. Hidden bool `yaml:"hidden"` } // UserQueryAttrs defines how user attributes are mapped to attributes in the // LDAP entry. type UserQueryAttrs struct { // ID defines the attribute used to identify a user. ID string `yaml:"id"` // UserQueryEmailAttr defines the attribute for a user e-mail. Email string `yaml:"email"` // UserQueryDisplayNameAttr defines the attribute for a user display name. // If not specified, "displayName" is used. DisplayName string `yaml:"display-name"` } type groupQueryArg struct { User string } // NewIdentityProvider creates a new LDAP identity provider. func NewIdentityProvider(p Params) (idp.IdentityProvider, error) { if p.Description == "" { p.Description = p.Name } if p.Icon == "" { p.Icon = "/static/images/icons/ldap.svg" } if p.UserQueryAttrs.ID == "" { return nil, errgo.Newf("missing 'id' config parameter in 'user-query-attrs'") } userQueryAttrs := []string{p.UserQueryAttrs.ID} if p.UserQueryAttrs.Email != "" { userQueryAttrs = append(userQueryAttrs, p.UserQueryAttrs.Email) } if p.UserQueryAttrs.DisplayName != "" { userQueryAttrs = append(userQueryAttrs, p.UserQueryAttrs.DisplayName) } if p.UserQueryFilter == "" { return nil, errgo.Newf("missing 'user-query-filter' config parameter") } if p.GroupQueryFilter == "" { return nil, errgo.Newf("missing 'group-query-filter' config parameter") } groupQueryFilterTemplate, err := template.New( "group-query-filter").Parse(p.GroupQueryFilter) if err != nil { return nil, errgo.Notef(err, "invalid 'group-query-filter' config parameter") } testFilter, err := renderTemplate(groupQueryFilterTemplate, groupQueryArg{User: "sample"}) if err != nil { return nil, errgo.Notef(err, "invalid 'group-query-filter' config parameter") } if _, err = ldap.CompileFilter(testFilter); err != nil { return nil, errgo.Notef(err, "invalid 'group-query-filter' config parameter") } idp := &identityProvider{ params: p, dialLDAP: dialLDAP, userQueryAttrs: userQueryAttrs, groupQueryFilterTemplate: groupQueryFilterTemplate, } u, err := url.Parse(p.URL) if err != nil { return nil, errgo.Notef(err, "cannot parse URL") } switch u.Scheme { case "ldap": idp.network = "tcp" // It would be nice to use u.Host and u.Port here, but // these aren't available in go 1.6. host, port, _ := net.SplitHostPort(u.Host) if host == "" { // Asume that the URL didn't specify a port. host = u.Host port = "ldap" } idp.address = net.JoinHostPort(host, port) idp.tlsConfig.ServerName = host default: // No other schemes are currently supported. return nil, errgo.Newf("unsupported scheme %q", u.Scheme) } idp.baseDN = strings.TrimPrefix(u.Path, "/") if p.CACertificate != "" { idp.tlsConfig.RootCAs = x509.NewCertPool() idp.tlsConfig.RootCAs.AppendCertsFromPEM([]byte(p.CACertificate)) } return idp, nil } type identityProvider struct { params Params initParams idp.InitParams dialLDAP func(network, addr string) (ldapConn, error) network string address string baseDN string tlsConfig tls.Config userQueryAttrs []string groupQueryFilterTemplate *template.Template } // Name implements idp.IdentityProvider.Name. func (idp *identityProvider) Name() string { return idp.params.Name } // Domain implements idp.IdentityProvider.Domain. func (idp *identityProvider) Domain() string { return idp.params.Domain } // Description implements idp.IdentityProvider.Description. func (idp *identityProvider) Description() string { return idp.params.Description } // IconURL returns the URL of an icon for the identity provider. func (idp *identityProvider) IconURL() string { return idputil.ServiceURL(idp.initParams.Location, idp.params.Icon) } // Interactive implements idp.IdentityProvider.Interactive. func (*identityProvider) Interactive() bool { return true } // Hidden implements idp.IdentityProvider.Hidden. func (idp *identityProvider) Hidden() bool { return idp.params.Hidden } // Init implements idp.IdentityProvider.Init. func (idp *identityProvider) Init(ctx context.Context, params idp.InitParams) error { idp.initParams = params return nil } // URL implements idp.IdentityProvider.URL. func (idp *identityProvider) URL(state string) string { return idputil.RedirectURL(idp.initParams.URLPrefix, "/login", state) } // URL implements idp.IdentityProvider.SetInteraction. func (idp *identityProvider) SetInteraction(ierr *httpbakery.Error, dischargeID string) { } // GetGroups implements idp.IdentityProvider.GetGroups. func (idp *identityProvider) GetGroups(ctx context.Context, identity *store.Identity) ([]string, error) { conn, err := idp.dial() if err != nil { return nil, errgo.Mask(err) } defer conn.Close() _, uid := identity.ProviderID.Split() filter, err := renderTemplate( idp.groupQueryFilterTemplate, groupQueryArg{User: ldap.EscapeFilter(uid)}) if err != nil { return nil, errgo.Mask(err) } logger.Tracef("LDAP groups search: basedn=%s scope=sub deref_aliases=never filter=%s attributes=[\"cn\"]", idp.baseDN, filter) req := &ldap.SearchRequest{ BaseDN: idp.baseDN, Scope: ldap.ScopeWholeSubtree, DerefAliases: ldap.NeverDerefAliases, Filter: filter, Attributes: []string{"cn"}, } res, err := conn.Search(req) if err != nil { logger.Tracef("LDAP search error: %s", err) return nil, errgo.Mask(err) } logResults(res) groups := []string{} for _, entry := range res.Entries { if entry == nil || len(entry.Attributes) == 0 || len(entry.Attributes[0].Values) == 0 { continue } groups = append(groups, entry.Attributes[0].Values[0]) } return groups, nil } // Handle implements idp.IdentityProvider.Handle. func (idp *identityProvider) Handle(ctx context.Context, w http.ResponseWriter, req *http.Request) { var ls idputil.LoginState state := req.Form.Get("state") if err := idp.initParams.Codec.Cookie(req, idputil.LoginCookieName, state, &ls); err != nil { logger.Infof("Invalid login state: %s", err) idputil.BadRequestf(w, "Login failed: invalid login state") return } switch strings.TrimPrefix(req.URL.Path, idp.initParams.URLPrefix) { case "/login": idpChoice := params.IDPChoiceDetails{ Domain: idp.params.Domain, Description: idp.params.Description, Name: idp.params.Name, URL: idp.URL(req.Form.Get("state")), } id, err := idputil.HandleLoginForm(ctx, w, req, idpChoice, idp.initParams.Template, idp.loginUser) if err != nil { idp.initParams.VisitCompleter.RedirectFailure(ctx, w, req, ls.ReturnTo, ls.State, err) } if id != nil { idp.initParams.VisitCompleter.RedirectMFA(ctx, w, req, idp.params.RequireMFA, ls.ReturnTo, ls.State, state, id) return } } } func (idp *identityProvider) loginUser(ctx context.Context, username, password string) (*store.Identity, error) { conn, err := idp.dial() if err != nil { return nil, errgo.Mask(err) } defer conn.Close() dn, err := idp.resolveUsername(conn, username) if err != nil { return nil, errgo.Mask(err) } id, err := idp.loginDN(ctx, conn, dn, password) if err != nil { if errgo.Cause(err) == params.ErrNotFound { return nil, errgo.Notef(err, "user %q not found", username) } return nil, errgo.Mask(err) } return id, nil } func (idp *identityProvider) loginDN(ctx context.Context, conn ldapConn, dn, password string) (*store.Identity, error) { logger.Tracef("LDAP bind: dn=%s", dn) if err := conn.Bind(dn, password); err != nil { logger.Tracef("LDAP bind error: %s", err) // Assume all bind errors represent invalid credentials, // other errors will have most likely been picked up // resolving the username. return nil, errgo.New("invalid username or password") } logger.Tracef("LDAP bind success") logger.Tracef("LDAP user search: basedn=%s scope=base deref_aliases=never filter=%s attributes=%s", dn, idp.params.UserQueryFilter, idp.userQueryAttrs) req := &ldap.SearchRequest{ BaseDN: dn, Scope: ldap.ScopeBaseObject, DerefAliases: ldap.NeverDerefAliases, SizeLimit: 1, Filter: idp.params.UserQueryFilter, Attributes: idp.userQueryAttrs, } res, err := conn.Search(req) if err != nil { logger.Tracef("LDAP search error: %s", err) return nil, errgo.Mask(err) } logResults(res) if len(res.Entries) == 0 { return nil, errgo.WithCausef(nil, params.ErrNotFound, "") } var username, email, name string for _, attr := range res.Entries[0].Attributes { switch attr.Name { case idp.params.UserQueryAttrs.ID: username = idputil.NameWithDomain(attr.Values[0], idp.params.Domain) case idp.params.UserQueryAttrs.Email: email = attr.Values[0] case idp.params.UserQueryAttrs.DisplayName: name = attr.Values[0] } } // set groups id := &store.Identity{ ProviderID: store.MakeProviderIdentity(idp.params.Name, dn), Username: username, Name: name, Email: email, } err = idp.initParams.Store.UpdateIdentity(ctx, id, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, }) if err != nil { return nil, errgo.Mask(err) } return id, nil } // resolveUsername returns the DN for a username func (idp *identityProvider) resolveUsername(conn ldapConn, username string) (string, error) { filter := fmt.Sprintf("(%s=%s)", idp.params.UserQueryAttrs.ID, ldap.EscapeFilter(username)) logger.Tracef("LDAP user search: basedn=%s scope=sub deref_aliases=never filter=%s", idp.baseDN, filter) req := &ldap.SearchRequest{ BaseDN: idp.baseDN, Scope: ldap.ScopeWholeSubtree, DerefAliases: ldap.NeverDerefAliases, SizeLimit: 1, Filter: filter, } res, err := conn.Search(req) if err != nil { logger.Tracef("LDAP search error: %s", err) return "", errgo.Mask(err) } logResults(res) if len(res.Entries) < 1 { return "", errgo.New("invalid username or password") } return res.Entries[0].DN, nil } // dial establishes a connection to the LDAP server and binds as the // search user (if specified). func (idp *identityProvider) dial() (ldapConn, error) { conn, err := idp.dialLDAP(idp.network, idp.address) if err != nil { return nil, errgo.Mask(err) } if err = conn.StartTLS(&idp.tlsConfig); err != nil { return nil, errgo.Mask(err) } if idp.params.DN != "" { logger.Tracef("LDAP bind: dn=%s", idp.params.DN) if err := conn.Bind(idp.params.DN, idp.params.Password); err != nil { logger.Tracef("LDAP bind error: %s", err) return nil, errgo.Mask(err) } logger.Tracef("LDAP bind success", err) } return conn, nil } func renderTemplate(tmpl *template.Template, ctx interface{}) (string, error) { var buf bytes.Buffer if err := tmpl.Execute(&buf, ctx); err != nil { return "", err } return buf.String(), nil } func dialLDAP(network, addr string) (ldapConn, error) { c, err := ldap.Dial(network, addr) if err != nil { return nil, err } return c, nil } // ldapConn represents the subset of ldap connection methods used // by the provider. It is defined so that it can be replaced for testing. type ldapConn interface { StartTLS(config *tls.Config) error Bind(username, password string) error Search(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) Close() } func logResults(res *ldap.SearchResult) { if logger.EffectiveLogLevel() > loggo.TRACE { return } logger.Tracef("LDAP search results:") for _, e := range res.Entries { logger.Tracef("\tDN=%s", e.DN) logger.Tracef("\tAttributes:") for _, a := range e.Attributes { logger.Tracef("\t\t%s=%s", a.Name, strings.Join(a.Values, ",")) } } } golang-github-canonical-candid-1.12.3/idp/ldap/ldap_test.go000066400000000000000000000317311457263123000235040ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package ldap_test import ( "context" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idptest" "github.com/canonical/candid/idp/ldap" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/store" ) const idpPrefix = "http://idp.example.com" type ldapSuite struct { idptest *idptest.Fixture } func TestLDAP(t *testing.T) { qtsuite.Run(qt.New(t), &ldapSuite{}) } func (s *ldapSuite) Init(c *qt.C) { s.idptest = idptest.NewFixture(c, candidtest.NewStore()) } var newTests = []struct { about string params ldap.Params expectError string }{{ about: "good params", params: ldap.Params{ Name: "ldap", URL: "ldap://localhost", UserQueryFilter: "(userAttr=val)", UserQueryAttrs: ldap.UserQueryAttrs{ID: "uid"}, GroupQueryFilter: "(groupAttr=val)", }, }, { about: "unparsable url", params: ldap.Params{ Name: "ldap", URL: "://", UserQueryFilter: "(userAttr=val)", UserQueryAttrs: ldap.UserQueryAttrs{ID: "uid"}, GroupQueryFilter: "(groupAttr=val)", }, expectError: `cannot parse URL: parse "?://"?: missing protocol scheme`, }, { about: "unsupported scheme", params: ldap.Params{ Name: "ldaps", URL: "ldaps://", UserQueryFilter: "(userAttr=val)", UserQueryAttrs: ldap.UserQueryAttrs{ID: "uid"}, GroupQueryFilter: "(groupAttr=val)", }, expectError: `unsupported scheme "ldaps"`, }, { about: "missing user query filter", params: ldap.Params{ Name: "ldap", URL: "://", UserQueryAttrs: ldap.UserQueryAttrs{ID: "uid"}, GroupQueryFilter: "(groupAttr=val)", }, expectError: `missing 'user-query-filter' config parameter`, }, { about: "missing group query filter", params: ldap.Params{ Name: "ldap", URL: "://", UserQueryFilter: "(userAttr=val)", UserQueryAttrs: ldap.UserQueryAttrs{ID: "uid"}, }, expectError: `missing 'group-query-filter' config parameter`, }, { about: "missing group attributes ID", params: ldap.Params{ Name: "ldap", URL: "://", UserQueryFilter: "(userAttr=val)", GroupQueryFilter: "(groupAttr=val)", }, expectError: `missing 'id' config parameter in 'user-query-attrs'`, }, { about: "invalid group query filter template", params: ldap.Params{ Name: "ldap", URL: "://", UserQueryFilter: "(userAttr=val)", UserQueryAttrs: ldap.UserQueryAttrs{ID: "uid"}, GroupQueryFilter: "{{.Invalid}}", }, expectError: `invalid 'group-query-filter' config parameter.*`, }, { about: "malformed group query filter", params: ldap.Params{ Name: "ldap", URL: "://", UserQueryFilter: "(userAttr=val)", UserQueryAttrs: ldap.UserQueryAttrs{ID: "uid"}, GroupQueryFilter: "{{.User", }, expectError: `invalid 'group-query-filter' config parameter.*`, }, { about: "invalid group query filter expression", params: ldap.Params{ Name: "ldap", URL: "://", UserQueryFilter: "(userAttr=val)", UserQueryAttrs: ldap.UserQueryAttrs{ID: "uid"}, GroupQueryFilter: "(invalid=", }, expectError: `invalid 'group-query-filter' config parameter.*`, }} func getSampleLdapDB() ldapDB { return ldapDB{{ // admin user (used for search binds) "dn": {"cn=test,dc=example,dc=com"}, "userPassword": {"pass"}, }, { "dn": {"uid=user1,ou=users,dc=example,dc=com"}, "objectClass": {"account"}, "uid": {"user1"}, "userPassword": {"pass1"}, }, { "dn": {"uid=user2,ou=users,dc=example,dc=com"}, "objectClass": {"account"}, "uid": {"user2"}, "userPassword": {"pass2"}, }} } func getSampleParams() ldap.Params { return ldap.Params{ Name: "test", URL: "ldap://localhost", DN: "cn=test,dc=example,dc=com", Password: "pass", UserQueryFilter: "(objectClass=account)", UserQueryAttrs: ldap.UserQueryAttrs{ID: "uid"}, GroupQueryFilter: "(&(objectClass=groupOfNames)(member={{.User}}))", } } func (s *ldapSuite) setupIdp(c *qt.C, params ldap.Params, db ldapDB) idp.IdentityProvider { i, err := ldap.NewIdentityProvider(params) c.Assert(err, qt.IsNil) ldap.SetLDAP(i, newMockLDAPDialer(db).Dial) i.Init(context.TODO(), s.idptest.InitParams(c, idpPrefix)) return i } func (s *ldapSuite) TestNewIdentityProvider(c *qt.C) { for _, test := range newTests { c.Run(test.about, func(c *qt.C) { idp, err := ldap.NewIdentityProvider(test.params) if test.expectError == "" { c.Assert(err, qt.IsNil) c.Assert(idp, qt.Not(qt.IsNil)) return } c.Assert(err, qt.ErrorMatches, test.expectError) c.Assert(idp, qt.IsNil) }) } } func (s *ldapSuite) TestName(c *qt.C) { idp, err := ldap.NewIdentityProvider(getSampleParams()) c.Assert(err, qt.IsNil) c.Assert(idp.Name(), qt.Equals, "test") } func (s *ldapSuite) TestDescription(c *qt.C) { params := getSampleParams() params.Description = "test description" idp, err := ldap.NewIdentityProvider(params) c.Assert(err, qt.IsNil) c.Assert(idp.Description(), qt.Equals, "test description") } func (s *ldapSuite) TestIconURL(c *qt.C) { i, err := ldap.NewIdentityProvider(getSampleParams()) c.Assert(err, qt.IsNil) err = i.Init(context.Background(), idp.InitParams{ Location: "https://www.example.com/candid", }) c.Assert(err, qt.IsNil) c.Assert(i.IconURL(), qt.Equals, "https://www.example.com/candid/static/images/icons/ldap.svg") } func (s *ldapSuite) TestAbsoluteIconURL(c *qt.C) { params := getSampleParams() params.Icon = "https://www.example.com/icon.bmp" idp, err := ldap.NewIdentityProvider(params) c.Assert(err, qt.IsNil) c.Assert(idp.IconURL(), qt.Equals, "https://www.example.com/icon.bmp") } func (s *ldapSuite) TestRelativeIconURL(c *qt.C) { params := getSampleParams() params.Icon = "/static/icon.bmp" i, err := ldap.NewIdentityProvider(params) c.Assert(err, qt.IsNil) err = i.Init(context.Background(), idp.InitParams{ Location: "https://www.example.com/candid", }) c.Assert(err, qt.IsNil) c.Assert(i.IconURL(), qt.Equals, "https://www.example.com/candid/static/icon.bmp") } func (s *ldapSuite) TestDomain(c *qt.C) { params := getSampleParams() params.Domain = "test domain" idp, err := ldap.NewIdentityProvider(params) c.Assert(err, qt.IsNil) c.Assert(idp.Domain(), qt.Equals, "test domain") } func (s *ldapSuite) TestInteractive(c *qt.C) { idp, err := ldap.NewIdentityProvider(getSampleParams()) c.Assert(err, qt.IsNil) c.Assert(idp.Interactive(), qt.Equals, true) } func (s *ldapSuite) TestHidden(c *qt.C) { idp, err := ldap.NewIdentityProvider(getSampleParams()) c.Assert(err, qt.IsNil) c.Assert(idp.Hidden(), qt.Equals, false) p := getSampleParams() p.Hidden = true idp, err = ldap.NewIdentityProvider(p) c.Assert(err, qt.IsNil) c.Assert(idp.Hidden(), qt.Equals, true) } func (s *ldapSuite) TestURL(c *qt.C) { i, err := ldap.NewIdentityProvider(getSampleParams()) c.Assert(err, qt.IsNil) i.Init(context.Background(), idp.InitParams{ URLPrefix: idpPrefix, }) c.Assert(i.URL("1"), qt.Equals, idpPrefix+"/login?state=1") } func (s *ldapSuite) TestHandle(c *qt.C) { params := getSampleParams() params.Domain = "ldap" i := s.setupIdp(c, params, getSampleLdapDB()) id, err := s.idptest.DoInteractiveLogin(c, i, idpPrefix+"/login", candidtest.PostLoginForm("user1", "pass1")) c.Assert(err, qt.IsNil) c.Assert(id.Username, qt.Equals, "user1@ldap") s.idptest.Store.AssertUser(c, &store.Identity{ ProviderID: store.MakeProviderIdentity( "test", "uid=user1,ou=users,dc=example,dc=com"), Username: "user1@ldap", }) } func (s *ldapSuite) TestHandleCustomUserFilter(c *qt.C) { params := getSampleParams() params.UserQueryFilter = "(customAttr=customValue)" sampleDB := getSampleLdapDB() sampleDB[1]["objectClass"] = []string{"ignored"} sampleDB[1]["customAttr"] = []string{"customValue"} i := s.setupIdp(c, params, sampleDB) id, err := s.idptest.DoInteractiveLogin(c, i, idpPrefix+"/login", candidtest.PostLoginForm("user1", "pass1")) c.Assert(err, qt.IsNil) c.Assert(id.Username, qt.Equals, "user1") s.idptest.Store.AssertUser(c, &store.Identity{ ProviderID: store.MakeProviderIdentity( "test", "uid=user1,ou=users,dc=example,dc=com"), Username: "user1", }) } func (s *ldapSuite) TestHandleUserDetails(c *qt.C) { params := getSampleParams() params.UserQueryAttrs.Email = "mail" params.UserQueryAttrs.DisplayName = "displayName" sampleDB := getSampleLdapDB() sampleDB[1]["mail"] = []string{"user1@example.com"} sampleDB[1]["displayName"] = []string{"User One"} i := s.setupIdp(c, params, sampleDB) id, err := s.idptest.DoInteractiveLogin(c, i, idpPrefix+"/login", candidtest.PostLoginForm("user1", "pass1")) c.Assert(err, qt.IsNil) c.Assert(id.Username, qt.Equals, "user1") s.idptest.Store.AssertUser(c, &store.Identity{ ProviderID: store.MakeProviderIdentity( "test", "uid=user1,ou=users,dc=example,dc=com"), Username: "user1", Name: "User One", Email: "user1@example.com", }) } func (s *ldapSuite) TestHandleUserDetailsCustomIDAttr(c *qt.C) { params := getSampleParams() params.UserQueryAttrs.ID = "myId" sampleDB := getSampleLdapDB() sampleDB[1]["uid"] = []string{"ignored"} sampleDB[1]["myId"] = []string{"user1"} i := s.setupIdp(c, params, sampleDB) id, err := s.idptest.DoInteractiveLogin(c, i, idpPrefix+"/login", candidtest.PostLoginForm("user1", "pass1")) c.Assert(err, qt.IsNil) c.Assert(id.Username, qt.Equals, "user1") s.idptest.Store.AssertUser(c, &store.Identity{ ProviderID: store.MakeProviderIdentity( "test", "uid=user1,ou=users,dc=example,dc=com"), Username: "user1", }) } func (s *ldapSuite) TestHandleWithGroups(c *qt.C) { docs := []ldapDoc{{ "dn": {"cn=group1,ou=users,dc=example,dc=com"}, "objectClass": {"groupOfNames"}, "cn": {"group1"}, "member": { "uid=user1,ou=users,dc=example,dc=com", "uid=user2,ou=users,dc=example,dc=com", }, }, { "dn": {"cn=group2,ou=users,dc=example,dc=com"}, "objectClass": {"groupOfNames"}, "cn": {"group2"}, "member": {"uid=user1,ou=users,dc=example,dc=com"}, }} sampleDB := append(getSampleLdapDB(), docs...) i := s.setupIdp(c, getSampleParams(), sampleDB) id, err := s.idptest.DoInteractiveLogin(c, i, idpPrefix+"/login", candidtest.PostLoginForm("user1", "pass1")) c.Assert(err, qt.IsNil) c.Assert(id.Username, qt.Equals, "user1") identity := s.idptest.Store.AssertUser(c, &store.Identity{ ProviderID: store.MakeProviderIdentity( "test", "uid=user1,ou=users,dc=example,dc=com"), Username: "user1", }) groups, err := i.GetGroups(s.idptest.Ctx, identity) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"group1", "group2"}) } func (s *ldapSuite) TestHandleCustomGroupFilter(c *qt.C) { params := getSampleParams() params.GroupQueryFilter = "(&(customAttr=customValue)(user={{.User}}))" docs := []ldapDoc{{ "dn": {"cn=group1,ou=users,dc=example,dc=com"}, "customAttr": {"customValue"}, "cn": {"group1"}, "user": { "uid=user1,ou=users,dc=example,dc=com", "uid=user2,ou=users,dc=example,dc=com", }, }, { "dn": {"cn=group2,ou=users,dc=example,dc=com"}, "customAttr": {"customValue"}, "cn": {"group2"}, "user": {"uid=user1,ou=users,dc=example,dc=com"}, }} sampleDB := append(getSampleLdapDB(), docs...) i := s.setupIdp(c, params, sampleDB) id, err := s.idptest.DoInteractiveLogin(c, i, idpPrefix+"/login", candidtest.PostLoginForm("user1", "pass1")) c.Assert(err, qt.IsNil) c.Assert(id.Username, qt.Equals, "user1") identity := s.idptest.Store.AssertUser(c, &store.Identity{ ProviderID: store.MakeProviderIdentity( "test", "uid=user1,ou=users,dc=example,dc=com"), Username: "user1", }) groups, err := i.GetGroups(s.idptest.Ctx, identity) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"group1", "group2"}) } func (s *ldapSuite) TestHandleIncorrectUsername(c *qt.C) { i := s.setupIdp(c, getSampleParams(), getSampleLdapDB()) _, err := s.idptest.DoInteractiveLogin(c, i, idpPrefix+"/login", candidtest.PostLoginForm("user-not-there", "wrong")) c.Assert(err, qt.ErrorMatches, `invalid username or password`) } func (s *ldapSuite) TestHandleFailedLogin(c *qt.C) { i := s.setupIdp(c, getSampleParams(), getSampleLdapDB()) _, err := s.idptest.DoInteractiveLogin(c, i, idpPrefix+"/login", candidtest.PostLoginForm("user1", "wrong")) c.Assert(err, qt.ErrorMatches, `invalid username or password`) } func (s *ldapSuite) TestHandleUserFilterNoMatch(c *qt.C) { params := getSampleParams() params.UserQueryFilter = "(customAttr=customValue)" sampleDB := getSampleLdapDB() sampleDB[1]["objectClass"] = []string{"ignored"} sampleDB[1]["customAttr"] = []string{"customValue2"} i := s.setupIdp(c, params, sampleDB) _, err := s.idptest.DoInteractiveLogin(c, i, idpPrefix+"/login", candidtest.PostLoginForm("user1", "pass1")) c.Assert(err, qt.ErrorMatches, `user "user1" not found: not found`) } golang-github-canonical-candid-1.12.3/idp/ldap/mockldap_test.go000066400000000000000000000102731457263123000243540ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package ldap_test import ( "crypto/tls" "fmt" ber "gopkg.in/asn1-ber.v1" errgo "gopkg.in/errgo.v1" "gopkg.in/ldap.v2" idpldap "github.com/canonical/candid/idp/ldap" ) type mockLDAPDialer struct { db ldapDB conns []*mockLDAPConn } func newMockLDAPDialer(db ldapDB) *mockLDAPDialer { d := &mockLDAPDialer{db: db} d.conns = []*mockLDAPConn{} return d } func (d *mockLDAPDialer) Dial(network, address string) (idpldap.LDAPConn, error) { conn := &mockLDAPConn{network: network, address: address, db: d.db} d.conns = append(d.conns, conn) return conn, nil } type mockLDAPConn struct { db ldapDB // network and address are set to the arguments passed to the dial // function. network string address string // tlsConfig is set when StartTLS is called. tlsConfig *tls.Config // searchReq is set when Search is called. searchReq *ldap.SearchRequest // boundUsername and boundPassword are set when Bind is called. boundUsername string boundPassword string // closed is set when Close is called. closed bool } func (c *mockLDAPConn) StartTLS(config *tls.Config) error { c.tlsConfig = config return nil } func (c *mockLDAPConn) Search(req *ldap.SearchRequest) (*ldap.SearchResult, error) { c.searchReq = req found, err := c.db.Search(req.Filter) if err != nil { return nil, err } entries := make([]*ldap.Entry, len(found)) for i, res := range found { attrs := []*ldap.EntryAttribute{} for _, name := range req.Attributes { values, ok := res[name] if !ok { continue } attrs = append( attrs, &ldap.EntryAttribute{ Name: name, Values: values, }) } entries[i] = &ldap.Entry{ DN: res["dn"][0], Attributes: attrs, } } return &ldap.SearchResult{Entries: entries}, nil } func (c *mockLDAPConn) Bind(username, password string) error { for _, entry := range c.db { dn, ok := entry["dn"] if !ok || len(dn) == 0 || dn[0] != username { continue } userPassword, ok := entry["userPassword"] if !ok || len(userPassword) == 0 { continue } if userPassword[0] == password { c.boundUsername = username c.boundPassword = password return nil } } return ldap.NewError(ldap.LDAPResultInvalidCredentials, errgo.New("invalid credentials")) } func (c *mockLDAPConn) Close() { c.closed = true } type ldapDoc map[string][]string type ldapDB []ldapDoc func (db ldapDB) Search(filter string) ([]ldapDoc, error) { match, err := filterMatcher(filter) if err != nil { return nil, err } var found []ldapDoc for _, doc := range db { if match(doc) { found = append(found, doc) } } return found, nil } // filterMatcher returns a function that reports whether a given LDAP document // matches the LDAP filter. It returns an error if the filter is malformed. func filterMatcher(filter string) (func(ldapDoc) bool, error) { packet, err := ldap.CompileFilter(filter) if err != nil { return nil, err } return packetFilterMatcher(packet), nil } func packetFilterMatcher(packet *ber.Packet) func(ldapDoc) bool { switch packet.Tag { case ldap.FilterAnd: var children []func(ldapDoc) bool for _, child := range packet.Children { children = append(children, packetFilterMatcher(child)) } return func(doc ldapDoc) bool { for _, child := range children { if !child(doc) { return false } } return true } case ldap.FilterOr: var children []func(ldapDoc) bool for _, child := range packet.Children { children = append(children, packetFilterMatcher(child)) } return func(doc ldapDoc) bool { for _, child := range children { if child(doc) { return true } } return true } case ldap.FilterNot: child := packetFilterMatcher(packet.Children[0]) return func(doc ldapDoc) bool { return !child(doc) } case ldap.FilterEqualityMatch: expected := string(packet.Children[1].Data.Bytes()) return func(doc ldapDoc) bool { values, ok := doc[string(packet.Children[0].Data.Bytes())] if !ok { return false } for _, value := range values { if value == expected { return true } } return false } default: panic(fmt.Sprintf("unimplemented tag: %v", packet.Tag)) } } golang-github-canonical-candid-1.12.3/idp/ldap/testdata/000077500000000000000000000000001457263123000230025ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/ldap/testdata/.dockerignore000066400000000000000000000000331457263123000254520ustar00rootroot00000000000000README docker-compose.yaml golang-github-canonical-candid-1.12.3/idp/ldap/testdata/00-config.ldif000066400000000000000000000002701457263123000253230ustar00rootroot00000000000000dn: cn=config objectClass: olcGlobal cn: config olcIdleTimeout: 30 olcLogLevel: Stats olcTLSCertificateFile: /srv/ldap/certs/cert.pem olcTLSCertificateKeyFile: /srv/ldap/certs/key.pem golang-github-canonical-candid-1.12.3/idp/ldap/testdata/0rg.ldif000066400000000000000000000001411457263123000243260ustar00rootroot00000000000000dn: dc=example,dc=com objectClass: dcObject objectClass: organization dc: example o: example.com golang-github-canonical-candid-1.12.3/idp/ldap/testdata/Dockerfile000066400000000000000000000004111457263123000247700ustar00rootroot00000000000000FROM docker.pkg.github.com/mhilton/openldap-docker/openldap ENV DB_ROOT_PASSWORD={SSHA}7S4I62IxUGCX+t3ivcGVXQQAxH5deFxy COPY *.pem /srv/ldap/certs/ COPY 00-config.ldif /srv/ldap/init/cn=config/ COPY 0rg.ldif group?.ldif user?.ldif /srv/ldap/init/dc=example,dc=com/ golang-github-canonical-candid-1.12.3/idp/ldap/testdata/README000066400000000000000000000046051457263123000236670ustar00rootroot00000000000000LDAP QA Environment =================== This directory contains the required configuration to bring up a working OpenLDAP deployment running in a container that can be used to QA the LDAP IDP. To bring up the QA environment simply run: docker-compose up This will run a TLS enabled LDAP server in a container listening on localhost port 389. To connect to the LDAP server add the following IDP definition to candidsrv's configuration: identity-providers: - type: ldap name: ldap domain: ldap url: ldap://localhost/dc=example,dc=com dn: cn=admin,dc=example,dc=com password: admin ca-cert: | -----BEGIN CERTIFICATE----- MIIC8jCCAdqgAwIBAgIUf56w/e4L0Qn7PwHIOs8wDftHC24wDQYJKoZIhvcNAQEL BQAwGTEXMBUGA1UEAxMOY2FuZGlkIHRlc3QgY2EwHhcNMTkwOTI3MTI1MTEzWhcN MzkxMjMxMTI1MTEzWjAZMRcwFQYDVQQDEw5jYW5kaWQgdGVzdCBjYTCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBANjvTTKgQCAKgOhppLP8FwZX4uB/9Pgs Np9euwmGGeQn1IMK94N1Q9CnxAgFrHPLen76VT/2/7AqRX1gkmeCG1D5Rv3xhNC1 LLH2mn4p2Vp8uSMhNyovwkuhNjH4q23rfleU1NZKijBYoPWIuoGQDVE/bztAI2Gm PbOtQCGag0Ta6ws1U5Fva4jlmqXnN4NQCNZ1jku+GdV0E3kxeRedhYfl6101tViZ sbILjHHvHkuvjYYwA0yYXHsQbudCzdsb0cUyNOBA/83NgGtCu/zbL6Cfe6yT5PrO NPAkX903qxYhKulcjMKQltgo6/EoULG/5P0GZ0zO3UGG7NtFGLr0ZHECAwEAAaMy MDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUiuUSWDgDSC5Qz6YpPHW6WM+l K7wwDQYJKoZIhvcNAQELBQADggEBAJl+DGvoVP3CW2vLiaLB/pLqnoUxmU0W6Bup DWj6S6AJSnQdxT5Nidgdna22NIATHC18ymr5DS36zC/eG9lZRwOg0jyGjBdIGtpq 1G9kC42sOq6o4pjWTu0WFWVo91FZNz1RLfVt2MYvVVn/fMJJISW2VxjYaC/jouIb ACU0gSgkckXUuhKiU1yBKpZ5dyRGaa7AtS8VzTefqLovfzPjyEc7dIO8IAXOokRx mhVZPUjSZ5YcbNufHvrjpcnvdYBpCrXx1XywdC3O0lw9Oiv9OWPbasEqb6ysRfQn LWSniske134cFVziC8wI5js3inQr+7zPPmfa1CRq8NyoqKuOVTQ= -----END CERTIFICATE----- user-query-filter: (objectClass=inetOrgPerson) user-query-attrs: id: uid email: mail display-name: displayName group-query-filter: (&(objectClass=groupOfNames)(member={{.User}})) The LDAP server is configured with the following users: - name: User One username: user1 password: password1 email: user1@example.com groups: - group1 - group3 - name: User Two username: user2 password: password2 email: user2@example.com groups: - group2 - group3 golang-github-canonical-candid-1.12.3/idp/ldap/testdata/cert.pem000066400000000000000000000022341457263123000244430ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDOjCCAiKgAwIBAgIVAMXnkSfQKsffoMs6Dagp3IbMIdw9MA0GCSqGSIb3DQEB CwUAMBkxFzAVBgNVBAMTDmNhbmRpZCB0ZXN0IGNhMB4XDTE5MDkyNzEzMTczNFoX DTM5MTIzMTEzMTczNFowFDESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtQWwbAhY2glsjV5Uf1xmDEsHYDZ5XJ79Qtd8 WTzAo7P384NR4sTZEgb7Yv2uYvBYOOaYMEo7QMH1q9d07+JkyjS2O4Pz1DDYNW5G 1eyX5sJHOW2EgNXpXvG50AnL1uAEwnVFN2ETVukb5Fas2f0ux+UFcPMk8ELg4228 hZIB3v/Z+LaZDASZ0EP/EeKolFwRS8GtsdQq1Sc1Xa/n7hprSEs5m4fNj0KqsVLb 3YMxo83T/ERTD8LXX1T8yRf08WR5C9DZGa76cPiuKx2PX12bWEpyQfOhU+V3+NUL Oor33aBYGOf68tYfC7dr7tBvOTvYItln/OiTwqKDmobOGdF1zwIDAQABo34wfDAM BgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRTns42M+iT+MavRQvBL8Kfd9EYNDAfBgNV HSMEGDAWgBSK5RJYOANILlDPpik8dbpYz6UrvDAsBgNVHREEJTAjgglsb2NhbGhv c3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggEBALrb /zfETKTqSQwzFrEyfQuLKdmZy+9tpUI92X3wabea8uH0tOaFElq2qegzKwqmxqJu 9vy9zl6fdufQUQjX9UQSadKH9wqtKkckiocHfK1olxCTyaw9QnPqNB0sqbHT1BG7 Jae0vP8T3lLJscGHV1q79rSufGLnxfffYE+tO4hOKwxXQ7wC+s7tp0b9K73OSE+0 r2DJZg3avvbCPRX1hcYOKn59G7jSO7q5RhwWlNru3fxHJHcVi0fklHDmx1nQ/ocn 1WQ6M5bkoLk7MFvJx3Fw1E90boyjVSb5WT3w+9zcZKv4VVshXgiszi1w4HBGjFv8 RB3FPYIoSBjm/PmgvME= -----END CERTIFICATE----- golang-github-canonical-candid-1.12.3/idp/ldap/testdata/docker-compose.yaml000066400000000000000000000001301457263123000265720ustar00rootroot00000000000000version: "2.0" services: ldap: build: context: . ports: - "389:389" golang-github-canonical-candid-1.12.3/idp/ldap/testdata/group1.ldif000066400000000000000000000001531457263123000250560ustar00rootroot00000000000000dn: cn=group1,dc=example,dc=com objectClass: groupOfNames cn: group1 member: cn=User One,dc=example,dc=com golang-github-canonical-candid-1.12.3/idp/ldap/testdata/group2.ldif000066400000000000000000000001531457263123000250570ustar00rootroot00000000000000dn: cn=group2,dc=example,dc=com objectClass: groupOfNames cn: group2 member: cn=User Two,dc=example,dc=com golang-github-canonical-candid-1.12.3/idp/ldap/testdata/group3.ldif000066400000000000000000000002211457263123000250540ustar00rootroot00000000000000dn: cn=group3,dc=example,dc=com objectClass: groupOfNames cn: group3 member: cn=User One,dc=example,dc=com member: cn=User Two,dc=example,dc=com golang-github-canonical-candid-1.12.3/idp/ldap/testdata/key.pem000066400000000000000000000032171457263123000243000ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEAtQWwbAhY2glsjV5Uf1xmDEsHYDZ5XJ79Qtd8WTzAo7P384NR 4sTZEgb7Yv2uYvBYOOaYMEo7QMH1q9d07+JkyjS2O4Pz1DDYNW5G1eyX5sJHOW2E gNXpXvG50AnL1uAEwnVFN2ETVukb5Fas2f0ux+UFcPMk8ELg4228hZIB3v/Z+LaZ DASZ0EP/EeKolFwRS8GtsdQq1Sc1Xa/n7hprSEs5m4fNj0KqsVLb3YMxo83T/ERT D8LXX1T8yRf08WR5C9DZGa76cPiuKx2PX12bWEpyQfOhU+V3+NULOor33aBYGOf6 8tYfC7dr7tBvOTvYItln/OiTwqKDmobOGdF1zwIDAQABAoIBAQCDmWs0Xq0ZdZhw /Y64OFgHv9PQ4klASGUd3sILrdruJ7uuUF2LWkfkjybaREouq0O3ugwRryV8YoVT NANTEWbsiX2hrr/oFspXrZ/ZjXKw07Qrz65waxKJb3oB/90sjRdotxBmvi/tEvKw AUMQBrunnptiDaFg+X28WN6gzBCIKIWfOC6K4E2lvZEULH1sEQWnpFUw6DFdiMGM jZRAsIRBQMdjqtS7x4b5rk64ipQwncj02u8eA+eN9EEKFqLZIGKghL9BFKgATWYL i0ZZVJNkJNZNbXV1kCOot7H6FEeYhOlG+qXgoF+eOrC/7OJcHINb04IyJWUNCrSS 6+XonDbBAoGBAO33ZMdGNg/ZpMnUtUCZScl6di9T9Om4y6eZnbuXhCa9NeV5jtD5 UDsoNzJ1iIfHJjeJlYHkewGmY74N5LNffKXIQsKhVoMDION5OGw0qW9f9uKgsxxz PwSHpDWosyyozYULWOgrPOWRJJYsVjlea/VhsTd0TwkXpWNu16/aC8d5AoGBAMK9 kJcaQgm/ik1HrcuTOhNqKXzVg8Tfm5XnlOjAvEiIDBrUprtXW9e/U8xyDWE4hov9 6dYV9w6Ei0n1mQTrMMXeemFRyb+5uez+A+eg8O2HngUu2POXhfgGuB3XGhcBxorc BL3SojqKGbUZIJhJP2+YOCtYTstTlEEAt7B2Iy2HAoGBAJEDHl21MoKsLZh0z9NC 1k06HEUxE2FOJITIvu8vIO/+g5aIYfiExViXZDOSnhWnzWkwpXQXSMIzpDqP+ts4 CBx6kfxLFw4VXPhhyXlfChV91FQ8e4mNzUw0YGP2nuFLKJq31ID+wEhhD3uYHx8s o/tPgg/6B6PRzg5u4G8gH+d5AoGBAKdE9EEVyMtevoiKnPlTSHsMoP4c18Xu21Dz TbBufEtAsEULivs3mifCq1PcUynCx67PNl8xFrhdmNa6IelbqtUKt8uH2ObZvJ2X wfre/pLH/i8tgiQZbnUQMG1RKZPBbqwvN9NkbPwjStmG77xejiJoeTah72wuKhV7 bEwh7S1ZAoGATzAFn3szxl2oIbNn25B6Cd6nZBDBqA3ZRFOeZbjaxu3h+K6Raq8F KTOvBbCWFST9kQtHKe07GHWzEKXPdwY2sEUoVwB3Z7GfQQ3tplaiwbBoEADETITu g8vkpujicr1bbKO9+/HXMFnfHwGgZnJRmvCy34FoaiGvmUMvh396h94= -----END RSA PRIVATE KEY----- golang-github-canonical-candid-1.12.3/idp/ldap/testdata/user1.ldif000066400000000000000000000003201457263123000246740ustar00rootroot00000000000000dn: cn=User One,dc=example,dc=com objectClass: inetOrgPerson cn: User One sn: One givenName: User uid: user1 userPassword: {SSHA}pBi6F1Wo+gk+wM9pPwKZCQ/kJTnEa8Oh mail: user1@example.com displayName: User One golang-github-canonical-candid-1.12.3/idp/ldap/testdata/user2.ldif000066400000000000000000000003211457263123000246760ustar00rootroot00000000000000dn: cn=User Two,dc=example,dc=com objectClass: inetOrgPerson cn: User Two sn: Two givenName: User uid: user2 userPassword: {SSHA}MpwAaXexjx3AMpCIchf9RlcF3LP6gTXB mail: user2@example.com displayName: User Two golang-github-canonical-candid-1.12.3/idp/openid/000077500000000000000000000000001457263123000215275ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/openid/openid-connect.go000066400000000000000000000375311457263123000247740ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package openid provides identity providers that use OpenID to // determine the identity. package openid import ( "context" "encoding/json" "fmt" "net/http" "regexp" "github.com/coreos/go-oidc" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/juju/loggo" "github.com/juju/names/v4" "golang.org/x/oauth2" "gopkg.in/errgo.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil" "github.com/canonical/candid/store" ) var logger = loggo.GetLogger("candid.idp.openid") func init() { idp.Register("openid-connect", func(unmarshal func(interface{}) error) (idp.IdentityProvider, error) { var p OpenIDConnectParams if err := unmarshal(&p); err != nil { return nil, errgo.Notef(err, "cannot unmarshal openid-connect parameters") } if p.Name == "" { return nil, errgo.Newf("name not specified") } if p.Issuer == "" { return nil, errgo.Newf("issuer not specified") } if p.ClientID == "" { return nil, errgo.Newf("client-id not specified") } if p.ClientSecret == "" { return nil, errgo.Newf("client-secret not specified") } return NewOpenIDConnectIdentityProvider(p), nil }) } // An IdentityCreator is used to create a candid identity from the // OAuth2 token returned by the OAuth2 authentication process. type IdentityCreator interface { // Create an identity using the provided token. The identity must // include a ProviderID which will remain constant for all // authentications made by the same user, it is recommended that the // ProviderID function is used for this purpose. // // If the identity includes a username then that username will be // used as the default when creating a new user. If a user already // exists that are identified by the ProviderID then the username // will not be updated. // // If the Name or Email values are non-zero these values will either // replace any currently stored values, or be used as defaults when // registering a new user. CreateIdentity(context.Context, *oauth2.Token) (store.Identity, error) } // A GroupsRetriever is used to retrieve a list of user groups from the // OpenID token returned by the OpenID authentication process. type GroupsRetriever interface { // RetrieveGroups retrieves groups from the OpenID token. RetrieveGroups(context.Context, *oauth2.Token, func(interface{}) error) ([]string, error) } type OpenIDConnectParams struct { // Name is the name that will be given to the identity provider. Name string `yaml:"name"` // Description is the description that will be used with the // identity provider. If this is not set then Name will be used. Description string `yaml:"description"` // Icon contains the URL or path of an icon. Icon string `yaml:"icon"` // Domain is the domain with which all identities created by this // identity provider will be tagged (not including the @ separator). Domain string `yaml:"domain"` // Issuer is the OpenID connect issuer for the identity provider. // Discovery will be performed for this issuer. Issuer string `yaml:"issuer"` // Scopes contains the OAuth scopes to request. Scopes []string `yaml:"scopes"` // ClientID is the ID of the client as registered with the issuer. ClientID string `yaml:"client-id"` // ClientSecret is a client specific secret agreed with the issuer. ClientSecret string `yaml:"client-secret"` // Hidden is set if the IDP should be hidden from interactive // prompts. Hidden bool `yaml:"hidden"` // MatchEmailAddr is a regular expression that is used to determine if // this identity provider can be used for a particular user email. MatchEmailAddr string `yaml:"match-email-addr"` // IdentityCreator is the IdentityCreator that the identity provider // will use to convert the OAuth2 token into a candid Identity. If // this is nil the default implementation provided by the // openIDConnect identity provider will be used. IdentityCreator IdentityCreator // GroupsRetriever is the GroupsRetriever that the identity provider // will use to retrieve a list of groups from the OAuth2 token. If // this is nil the default implementation provided by the // openIDConnect identity provider will be used. GroupsRetriever GroupsRetriever } // NewOpenIDConnectIdentityProvider creates a new identity provider using // OpenID connect. func NewOpenIDConnectIdentityProvider(params OpenIDConnectParams) idp.IdentityProvider { if params.Description == "" { params.Description = params.Name } if params.Icon == "" { params.Icon = "/static/images/icons/openid.svg" } if len(params.Scopes) == 0 { params.Scopes = []string{oidc.ScopeOpenID} } var matchEmailAddr *regexp.Regexp if params.MatchEmailAddr != "" { var err error matchEmailAddr, err = regexp.Compile(params.MatchEmailAddr) if err != nil { // if the email address matcher doesn't compile log the error but // carry on. A regular expression that doesn't compile also doesn't // match anything. logger.Errorf("cannot compile match-email-addr regular expression: %s", err) } } return &openidConnectIdentityProvider{ params: params, matchEmailAddr: matchEmailAddr, } } type openidConnectIdentityProvider struct { params OpenIDConnectParams initParams idp.InitParams provider *oidc.Provider config *oauth2.Config matchEmailAddr *regexp.Regexp } // Name implements idp.IdentityProvider.Name. func (idp *openidConnectIdentityProvider) Name() string { return idp.params.Name } // Domain implements idp.IdentityProvider.Domain. func (idp *openidConnectIdentityProvider) Domain() string { return idp.params.Domain } // Description implements idp.IdentityProvider.Description. func (idp *openidConnectIdentityProvider) Description() string { return idp.params.Description } // IconURL returns the URL of an icon for the identity provider. func (idp *openidConnectIdentityProvider) IconURL() string { return idputil.ServiceURL(idp.initParams.Location, idp.params.Icon) } // Interactive implements idp.IdentityProvider.Interactive. func (*openidConnectIdentityProvider) Interactive() bool { return true } // Hidden implements idp.IdentityProvider.Hidden. func (idp *openidConnectIdentityProvider) Hidden() bool { return idp.params.Hidden } // IsForEmailAddr returns true when the identity provider should be used // to identify a user with the given email address. func (idp *openidConnectIdentityProvider) IsForEmailAddr(addr string) bool { if idp.matchEmailAddr == nil { return false } return idp.matchEmailAddr.MatchString(addr) } // Init implements idp.IdentityProvider.Init by performing discovery on // the issuer and set up the identity provider. func (idp *openidConnectIdentityProvider) Init(ctx context.Context, params idp.InitParams) error { idp.initParams = params var err error idp.provider, err = oidc.NewProvider(ctx, idp.params.Issuer) if err != nil { return errgo.Mask(err) } idp.config = &oauth2.Config{ ClientID: idp.params.ClientID, ClientSecret: idp.params.ClientSecret, Endpoint: idp.provider.Endpoint(), RedirectURL: idp.initParams.URLPrefix + "/callback", Scopes: idp.params.Scopes, } return nil } // URL implements idp.IdentityProvider.URL. func (idp *openidConnectIdentityProvider) URL(state string) string { return idputil.RedirectURL(idp.initParams.URLPrefix, "/login", state) } // SetInteraction implements idp.IdentityProvider.SetInteraction. func (idp *openidConnectIdentityProvider) SetInteraction(ierr *httpbakery.Error, dischargeID string) { } // GetGroups implements idp.IdentityProvider.GetGroups. func (idp *openidConnectIdentityProvider) GetGroups(_ context.Context, identity *store.Identity) ([]string, error) { return identity.Groups, nil } // Handle implements idp.IdentityProvider.Handle. func (idp *openidConnectIdentityProvider) Handle(ctx context.Context, w http.ResponseWriter, req *http.Request) { var ls idputil.LoginState if err := idp.initParams.Codec.Cookie(req, idputil.LoginCookieName, req.Form.Get("state"), &ls); err != nil { logger.Infof("Invalid login state: %s", err) idputil.BadRequestf(w, "Login failed: invalid login state") return } switch req.URL.Path { case "/callback": if err := idp.callback(ctx, w, req, ls); err != nil { idp.initParams.VisitCompleter.RedirectFailure(ctx, w, req, ls.ReturnTo, ls.State, err) } case "/register": if err := idp.register(ctx, w, req, ls); err != nil { idp.initParams.VisitCompleter.RedirectFailure(ctx, w, req, ls.ReturnTo, ls.State, err) } default: idp.login(ctx, w, req) } } func (idp *openidConnectIdentityProvider) login(ctx context.Context, w http.ResponseWriter, req *http.Request) { http.Redirect(w, req, idp.config.AuthCodeURL(idputil.State(req)), http.StatusFound) } func (idp *openidConnectIdentityProvider) callback(ctx context.Context, w http.ResponseWriter, req *http.Request, ls idputil.LoginState) error { tok, err := idp.config.Exchange(ctx, req.Form.Get("code")) if err != nil { return errgo.Mask(err) } ic := idp.params.IdentityCreator if ic == nil { ic = idp } user, err := ic.CreateIdentity(ctx, tok) if err != nil { return errgo.Mask(err) } existingUser := store.Identity{ ProviderID: user.ProviderID, } err = idp.initParams.Store.Identity(ctx, &existingUser) if err == nil { var upd store.Update // A user exists check if it needs updating. if user.Name != "" && existingUser.Name != user.Name { existingUser.Name = user.Name upd[store.Name] = store.Set } if user.Email != "" && existingUser.Email != user.Email { existingUser.Email = user.Email upd[store.Email] = store.Set } if !cmp.Equal(user.Groups, existingUser.Groups, cmpopts.SortSlices(func(a, b string) bool { return a < b })) { existingUser.Groups = user.Groups upd[store.Groups] = store.Set } if (upd != store.Update{}) { err = idp.initParams.Store.UpdateIdentity(ctx, &existingUser, upd) } if err == nil { idp.initParams.VisitCompleter.RedirectSuccess(ctx, w, req, ls.ReturnTo, ls.State, &existingUser) return nil } } if errgo.Cause(err) != store.ErrNotFound { return errgo.Mask(err) } // The user needs to be created. if user.Username != "" { // Attempt to create a user with the preferred username. err := idp.initParams.Store.UpdateIdentity(ctx, &user, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, store.Groups: store.Set, }) if err == nil { idp.initParams.VisitCompleter.RedirectSuccess(ctx, w, req, ls.ReturnTo, ls.State, &user) return nil } if errgo.Cause(err) != store.ErrDuplicateUsername { return errgo.Mask(err) } } // The user needs to register. ls.ProviderID = user.ProviderID cookiePath := idputil.CookiePathRelativeToLocation(idputil.LoginCookiePath, idp.initParams.Location, idp.initParams.SkipLocationForCookiePaths) state, err := idp.initParams.Codec.SetCookie(w, idputil.LoginCookieName, cookiePath, ls) if err != nil { return errgo.Mask(err) } var groups string if len(user.Groups) > 0 { groupData, err := json.Marshal(user.Groups) if err != nil { return errgo.Mask(err) } groups = string(groupData) } return errgo.Mask(idputil.RegistrationForm(ctx, w, idputil.RegistrationParams{ State: state, Domain: idp.params.Domain, FullName: user.Name, Email: user.Email, Groups: groups, }, idp.initParams.Template)) } func (idp *openidConnectIdentityProvider) register(ctx context.Context, w http.ResponseWriter, req *http.Request, ls idputil.LoginState) error { var groups []string groupsString := req.Form.Get("groups") if groupsString != "" { err := json.Unmarshal([]byte(groupsString), &groups) if err != nil { return errgo.Mask(err) } } u := &store.Identity{ ProviderID: ls.ProviderID, Name: req.Form.Get("fullname"), Email: req.Form.Get("email"), Groups: groups, } err := idp.registerUser(ctx, req.Form.Get("username"), u) if err == nil { idp.initParams.VisitCompleter.RedirectSuccess(ctx, w, req, ls.ReturnTo, ls.State, u) return nil } if errgo.Cause(err) != errInvalidUser { return errgo.Mask(err) } return errgo.Mask(idputil.RegistrationForm(ctx, w, idputil.RegistrationParams{ State: req.Form.Get("state"), Error: err.Error(), Username: req.Form.Get("username"), Domain: idp.params.Domain, FullName: req.Form.Get("fullname"), Email: req.Form.Get("email"), Groups: req.Form.Get("groups"), }, idp.initParams.Template)) } var errInvalidUser = errgo.New("invalid user") func (idp *openidConnectIdentityProvider) registerUser(ctx context.Context, username string, u *store.Identity) error { if !names.IsValidUserName(username) { return errgo.WithCausef(nil, errInvalidUser, "invalid user name. The username must contain only A-Z, a-z, 0-9, '.', '-', & '+', and must start and end with a letter or number.") } if idputil.ReservedUsernames[username] { return errgo.WithCausef(nil, errInvalidUser, "username %s is not allowed, please choose another.", username) } u.Username = joinDomain(username, idp.params.Domain) err := idp.initParams.Store.UpdateIdentity(ctx, u, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, store.Groups: store.Set, }) if err == nil { return nil } if errgo.Cause(err) != store.ErrDuplicateUsername { return errgo.Mask(err) } return errgo.WithCausef(nil, errInvalidUser, "Username already taken, please pick a different one.") } // CreateIdentity is the default implementation of an IdentityCreator. // CreateIdentity creates the identity from the "id_token" attached to // the given token. The ProviderID will be created using the ProviderID // function. The Username, Name & Email values will be taken from the // claims "preferred_username", "name" & "email" if they are present. func (idp *openidConnectIdentityProvider) CreateIdentity(ctx context.Context, tok *oauth2.Token) (store.Identity, error) { idtok := tok.Extra("id_token") if idtok == nil { return store.Identity{}, errgo.Newf("no id_token in OpenID response") } idtoks, ok := idtok.(string) if !ok { return store.Identity{}, errgo.Newf("invalid id_token in OpenID response") } id, err := idp.provider.Verifier(&oidc.Config{ClientID: idp.config.ClientID}).Verify(ctx, idtoks) if err != nil { return store.Identity{}, errgo.Mask(err) } user := store.Identity{ ProviderID: ProviderID(idp.Name(), id), } var claims claims if err := id.Claims(&claims); err == nil { if names.IsValidUserName(claims.PreferredUsername) { user.Username = joinDomain(claims.PreferredUsername, idp.Domain()) } user.Email = claims.Email user.Name = claims.FullName if idp.params.GroupsRetriever != nil { if user.Groups, err = idp.params.GroupsRetriever.RetrieveGroups(ctx, tok, id.Claims); err != nil { return store.Identity{}, errgo.Notef(err, "failed to retrieve groups from an OpenID response") } } else { user.Groups = claims.Groups } } return user, nil } // claims contains the set of claims possibly returned in the OpenID // token. type claims struct { FullName string `json:"name"` Email string `json:"email"` PreferredUsername string `json:"preferred_username"` Groups []string `json:"groups"` } // joinDomain creates a new params.Username with the given name and // (optional) domain. func joinDomain(name, domain string) string { if domain == "" { return name } return fmt.Sprintf("%s@%s", name, domain) } // registrationState holds state information about a registration that is // in progress. type registrationState struct { WaitID string `json:"wid"` ProviderID store.ProviderIdentity `json:"pid"` } // ProviderID creates a ProviderIdentity using the Subject and Issuer // from the given ID token. func ProviderID(provider string, id *oidc.IDToken) store.ProviderIdentity { return store.MakeProviderIdentity(provider, fmt.Sprintf("%s:%s", id.Issuer, id.Subject)) } golang-github-canonical-candid-1.12.3/idp/openid/openid-connect_test.go000066400000000000000000000446541457263123000260370ustar00rootroot00000000000000// Copyright 2021 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package openid_test import ( "context" "crypto/rand" "crypto/rsa" "encoding/base64" "encoding/json" "html/template" "math/big" "net/http" "net/http/httptest" "net/url" "strings" "sync" "testing" "time" qt "github.com/frankban/quicktest" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/jwt" "gopkg.in/yaml.v2" "github.com/canonical/candid/config" idppkg "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idptest" "github.com/canonical/candid/idp/idputil" "github.com/canonical/candid/idp/openid" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/store" ) var configTests = []struct { name string yaml string expectError string }{{ name: "OK", yaml: ` identity-providers: - type: openid-connect name: test issuer: example.com client-id: test-client-id client-secret: test-client-secret `[1:], }, { name: "NoName", yaml: ` identity-providers: - type: openid-connect issuer: example.com client-id: test-client-id client-secret: test-client-secret `[1:], expectError: "cannot unmarshal openid-connect configuration: name not specified", }, { name: "NoIssuer", yaml: ` identity-providers: - type: openid-connect name: test client-id: test-client-id client-secret: test-client-secret `[1:], expectError: "cannot unmarshal openid-connect configuration: issuer not specified", }, { name: "NoClientID", yaml: ` identity-providers: - type: openid-connect name: test issuer: example.com client-secret: test-client-secret `[1:], expectError: "cannot unmarshal openid-connect configuration: client-id not specified", }, { name: "NoClientSecret", yaml: ` identity-providers: - type: openid-connect name: test issuer: example.com client-id: test-client-id `[1:], expectError: "cannot unmarshal openid-connect configuration: client-secret not specified", }} func TestConfig(t *testing.T) { c := qt.New(t) for _, test := range configTests { c.Run(test.name, func(c *qt.C) { var conf config.Config err := yaml.Unmarshal([]byte(test.yaml), &conf) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) return } c.Assert(err, qt.IsNil) c.Assert(conf.IdentityProviders, qt.HasLen, 1) c.Assert(conf.IdentityProviders[0].Name(), qt.Equals, "test") }) } } func TestName(t *testing.T) { c := qt.New(t) idp := openid.NewOpenIDConnectIdentityProvider(openid.OpenIDConnectParams{ Name: "abcdef", }) c.Check(idp.Name(), qt.Equals, "abcdef") } func TestDomain(t *testing.T) { c := qt.New(t) idp := openid.NewOpenIDConnectIdentityProvider(openid.OpenIDConnectParams{ Name: "abcdef", Domain: "ghijklmn", }) c.Check(idp.Domain(), qt.Equals, "ghijklmn") } func TestDescription(t *testing.T) { c := qt.New(t) idp := openid.NewOpenIDConnectIdentityProvider(openid.OpenIDConnectParams{ Description: "test openid-connect idp", }) c.Check(idp.Description(), qt.Equals, "test openid-connect idp") } func TestInteractive(t *testing.T) { c := qt.New(t) idp := openid.NewOpenIDConnectIdentityProvider(openid.OpenIDConnectParams{}) c.Check(idp.Interactive(), qt.Equals, true) } func TestHidden(t *testing.T) { c := qt.New(t) idp := openid.NewOpenIDConnectIdentityProvider(openid.OpenIDConnectParams{ Hidden: true, }) c.Check(idp.Hidden(), qt.Equals, true) } func TestIsForEmailAddr(t *testing.T) { c := qt.New(t) idp := openid.NewOpenIDConnectIdentityProvider(openid.OpenIDConnectParams{}) type ifea interface { IsForEmailAddr(string) bool } c.Check(idp.(ifea).IsForEmailAddr("me@example.com"), qt.Equals, false) idp = openid.NewOpenIDConnectIdentityProvider(openid.OpenIDConnectParams{ MatchEmailAddr: "@example.com$", }) c.Check(idp.(ifea).IsForEmailAddr("me@example.com"), qt.Equals, true) c.Check(idp.(ifea).IsForEmailAddr("me@example.net"), qt.Equals, false) c.Check(idp.(ifea).IsForEmailAddr("me@example.com.zz"), qt.Equals, false) } func TestURL(t *testing.T) { c := qt.New(t) mux := http.NewServeMux() srv := httptest.NewServer(mux) defer srv.Close() idp := openid.NewOpenIDConnectIdentityProvider(openid.OpenIDConnectParams{ Issuer: srv.URL, }) mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, _ *http.Request) { conf := map[string]string{ "issuer": srv.URL, } e := json.NewEncoder(w) if err := e.Encode(conf); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }) err := idp.Init(context.Background(), idppkg.InitParams{ URLPrefix: "https://example.com/login/oidc", }) c.Assert(err, qt.IsNil) c.Check(idp.URL("1234"), qt.Equals, "https://example.com/login/oidc/login?state=1234") } func TestIconURL(t *testing.T) { c := qt.New(t) mux := http.NewServeMux() srv := httptest.NewServer(mux) defer srv.Close() idp := openid.NewOpenIDConnectIdentityProvider(openid.OpenIDConnectParams{ Issuer: srv.URL, Icon: "static/oidc.ico", }) mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, _ *http.Request) { conf := map[string]string{ "issuer": srv.URL, } e := json.NewEncoder(w) if err := e.Encode(conf); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }) err := idp.Init(context.Background(), idppkg.InitParams{ Location: "https://example.com", }) c.Assert(err, qt.IsNil) c.Check(idp.IconURL(), qt.Equals, "https://example.com/static/oidc.ico") } func TestHandleLogin(t *testing.T) { c := qt.New(t) srv := newTestOIDCServer() defer srv.Close() idp := openid.NewOpenIDConnectIdentityProvider(openid.OpenIDConnectParams{ Issuer: srv.URL, ClientID: "test-client-id", Scopes: []string{"openid", "email"}, }) f := idptest.NewFixture(c, candidtest.NewStore()) ip := f.InitParams(c, "http://example.com/login/oidc") err := idp.Init(context.Background(), ip) c.Assert(err, qt.IsNil) cl := idptest.NewClient(idp, ip.Codec) cl.SetLoginState(idputil.LoginState{ ReturnTo: "http://example.com/callback", State: "1234", Expires: time.Now().Add(10 * time.Minute), }) resp, err := cl.Get("/login") c.Assert(err, qt.IsNil) c.Check(resp.StatusCode, qt.Equals, http.StatusFound, qt.Commentf(resp.Status)) u, err := url.Parse(resp.Header.Get("Location")) c.Assert(err, qt.IsNil) c.Check(u.Scheme+"://"+u.Host+u.Path, qt.Equals, srv.URL+"/auth") vs := u.Query() c.Check(vs.Get("state"), qt.Not(qt.Equals), "") c.Check(vs.Get("client_id"), qt.Equals, "test-client-id") c.Check(vs.Get("redirect_uri"), qt.Equals, "http://example.com/login/oidc/callback") c.Check(vs.Get("scope"), qt.Equals, "openid email") } var handleCallbackTests = []struct { name string storedIdentities func(string) []store.Identity claims map[string]interface{} expectIdentity func(string) store.Identity }{{ name: "NewUserWithUsername", claims: map[string]interface{}{ "sub": "user-id-1", "preferred_username": "user1", "email": "user1@example.com", "name": "User One", }, expectIdentity: func(s string) store.Identity { return store.Identity{ ProviderID: store.ProviderIdentity("oidc:" + s + ":user-id-1"), Username: "user1", Email: "user1@example.com", Name: "User One", } }, }, { name: "NewUserNoUsername", claims: map[string]interface{}{ "sub": "user-id-1", "email": "user1@example.com", "name": "User One", }, }, { name: "ExistingUserNoClaims", storedIdentities: func(s string) []store.Identity { return []store.Identity{{ ProviderID: store.ProviderIdentity("oidc:" + s + ":user-id-1"), Username: "user1", Email: "user1@example.com", Name: "User One", }} }, claims: map[string]interface{}{ "sub": "user-id-1", }, expectIdentity: func(s string) store.Identity { return store.Identity{ ProviderID: store.ProviderIdentity("oidc:" + s + ":user-id-1"), Username: "user1", Email: "user1@example.com", Name: "User One", } }, }, { name: "ExistingUserUpdateClaims", storedIdentities: func(s string) []store.Identity { return []store.Identity{{ ProviderID: store.ProviderIdentity("oidc:" + s + ":user-id-1"), Username: "user1", Email: "user1@example.com", Name: "User One", }} }, claims: map[string]interface{}{ "sub": "user-id-1", "preferred_username": "user0", "email": "user0@example.com", "name": "User Zero", "groups": []string{"group1", "group2"}, }, expectIdentity: func(s string) store.Identity { return store.Identity{ ProviderID: store.ProviderIdentity("oidc:" + s + ":user-id-1"), Username: "user1", Email: "user0@example.com", Name: "User Zero", Groups: []string{"group1", "group2"}, } }, }, { name: "PreferredUsernameTaken", storedIdentities: func(s string) []store.Identity { return []store.Identity{{ ProviderID: store.ProviderIdentity("oidc:" + s + ":user-id-1"), Username: "user1", Email: "user1@example.com", Name: "User One", }} }, claims: map[string]interface{}{ "sub": "user-id-2", "preferred_username": "user1", "email": "user1@example.com", "name": "User One", }, }} func TestHandleCallback(t *testing.T) { c := qt.New(t) for _, test := range handleCallbackTests { c.Run(test.name, func(c *qt.C) { srv := newTestOIDCServer() defer srv.Close() p := openid.OpenIDConnectParams{ Name: "oidc", Issuer: srv.URL, } p.ClientID, p.ClientSecret = srv.clientCreds() idp := openid.NewOpenIDConnectIdentityProvider(p) st := candidtest.NewStore() f := idptest.NewFixture(c, st) ip := f.InitParams(c, "http://example.com/login/oidc") ip.Template = template.New("") template.Must(ip.Template.New("register").Parse("{{.State}}\n{{.Error}}")) err := idp.Init(context.Background(), ip) c.Assert(err, qt.IsNil) if test.storedIdentities != nil { for _, id := range test.storedIdentities(srv.URL) { err := st.Store.UpdateIdentity(context.Background(), &id, store.Update{ store.Username: store.Set, store.Email: store.Set, store.Name: store.Set, store.Groups: store.Set, }) c.Assert(err, qt.IsNil) } } cl := idptest.NewClient(idp, ip.Codec) cl.SetLoginState(idputil.LoginState{ ReturnTo: "http://example.com/callback", State: "1234", Expires: time.Now().Add(10 * time.Minute), }) srv.setClaim("aud", p.ClientID) srv.setClaim("exp", time.Now().Add(time.Minute).Unix()) srv.setClaim("iat", time.Now().Unix()) for k, v := range test.claims { srv.setClaim(k, v) } resp, err := cl.Get("/callback?code=" + srv.code()) id, err := f.ParseResponse(c, resp) c.Assert(err, qt.IsNil) if test.expectIdentity == nil { c.Check(id, qt.IsNil) } else { id.ID = "" expectID := test.expectIdentity(srv.URL) c.Check(id, qt.CmpEquals(cmpopts.EquateEmpty()), &expectID) } }) } } var handleRegisterTests = []struct { name string storedIdentities []store.Identity username string fullname string email string groups []string expectIdentity store.Identity expectError string }{{ name: "Success", username: "user1", fullname: "User One", email: "user1@example.com", groups: []string{"group1", "group2"}, expectIdentity: store.Identity{ ProviderID: "oidc:example.com:user-id-1", Username: "user1@test", Name: "User One", Email: "user1@example.com", Groups: []string{"group1", "group2"}, }, }, { name: "InvalidUsername", username: "!", fullname: "User One", email: "user1@example.com", expectError: "invalid user name. The username must contain only A-Z, a-z, 0-9, '.', '-', & '+', and must start and end with a letter or number.", }, { name: "InvalidUsername", storedIdentities: []store.Identity{{ ProviderID: "oidc:example.com:user-id-0", Username: "user1@test", }}, username: "user1", fullname: "User One", email: "user1@example.com", expectError: "Username already taken, please pick a different one.", }} func TestHandleRegister(t *testing.T) { c := qt.New(t) for _, test := range handleRegisterTests { c.Run(test.name, func(c *qt.C) { srv := newTestOIDCServer() defer srv.Close() p := openid.OpenIDConnectParams{ Name: "oidc", Domain: "test", Issuer: srv.URL, } idp := openid.NewOpenIDConnectIdentityProvider(p) st := candidtest.NewStore() f := idptest.NewFixture(c, st) ip := f.InitParams(c, "http://example.com/login/oidc") ip.Template = template.New("") template.Must(ip.Template.New("register").Parse("{{.State}}\n{{.Error}}")) err := idp.Init(context.Background(), ip) c.Assert(err, qt.IsNil) for _, id := range test.storedIdentities { err := st.Store.UpdateIdentity(context.Background(), &id, store.Update{ store.Username: store.Set, store.Email: store.Set, store.Name: store.Set, store.Groups: store.Set, }) c.Assert(err, qt.IsNil) } cl := idptest.NewClient(idp, ip.Codec) cl.SetLoginState(idputil.LoginState{ ProviderID: "oidc:example.com:user-id-1", ReturnTo: "http://example.com/callback", State: "1234", Expires: time.Now().Add(10 * time.Minute), }) vs := make(url.Values) if test.username != "" { vs.Set("username", test.username) } if test.fullname != "" { vs.Set("fullname", test.fullname) } if test.email != "" { vs.Set("email", test.email) } if test.groups != nil { data, err := json.Marshal(test.groups) c.Assert(err, qt.IsNil) vs.Set("groups", string(data)) } req, err := http.NewRequest("POST", "/register", strings.NewReader(vs.Encode())) c.Assert(err, qt.IsNil) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := cl.Do(req) c.Assert(err, qt.IsNil) id, err := f.ParseResponse(c, resp) if test.expectError != "" { c.Check(err, qt.ErrorMatches, test.expectError) return } if test.expectIdentity.ProviderID != "" { c.Check(err, qt.IsNil) id.ID = "" c.Check(id, qt.CmpEquals(cmpopts.EquateEmpty()), &test.expectIdentity) } }) } } type testOIDCServer struct { *httptest.Server mu sync.Mutex clientID, clientSecret string claims_ map[string]interface{} code_ string key_ *rsa.PrivateKey } func newTestOIDCServer() *testOIDCServer { var srv testOIDCServer srv.Server = httptest.NewServer(&srv) return &srv } func (s *testOIDCServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { req.ParseForm() switch req.URL.Path { case "/.well-known/openid-configuration": s.serveConfiguration(w, req) case "/token": s.serveToken(w, req) case "/keys": s.serveKeys(w, req) default: http.NotFound(w, req) } } func (s *testOIDCServer) serveConfiguration(w http.ResponseWriter, req *http.Request) { conf := map[string]interface{}{ "issuer": s.URL, "authorization_endpoint": s.URL + "/auth", "token_endpoint": s.URL + "/token", "jwks_uri": s.URL + "/keys", "id_token_signing_alg_values_supported": []string{"RS256"}, } buf, err := json.Marshal(conf) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } w.Header().Set("Content-Type", "application/json") w.Write(buf) } func (s *testOIDCServer) serveToken(w http.ResponseWriter, req *http.Request) { clientID, clientSecret := s.clientCreds() user, pw, ok := req.BasicAuth() var authenticated bool if ok { authenticated = (user == clientID && pw == clientSecret) } else { authenticated = (req.Form.Get("client_id") == clientID && req.Form.Get("client_secret") == clientSecret) } if !authenticated { w.Header().Set("WWW-Authenticate", "Basic realm=test-oidc") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"error":"invalid_client"}`)) return } if req.Form.Get("code") != s.code() { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"invalid_grant"}`)) return } tok := map[string]string{ "access_token": uuid.New().String(), } signer, err := jose.NewSigner(jose.SigningKey{jose.RS256, s.key()}, nil) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } claims := s.claims() if claims["iss"] == nil { claims["iss"] = s.URL } tok["id_token"], err = jwt.Signed(signer).Claims(claims).CompactSerialize() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } buf, err := json.Marshal(tok) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(buf) } func (s *testOIDCServer) serveKeys(w http.ResponseWriter, req *http.Request) { jwk := map[string]string{ "kty": "RSA", "alg": "RS256", "n": base64.RawURLEncoding.EncodeToString(s.key().PublicKey.N.Bytes()), "e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(s.key().PublicKey.E)).Bytes()), } keys := map[string][]map[string]string{ "keys": []map[string]string{jwk}, } buf, err := json.Marshal(keys) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(buf) } func (s *testOIDCServer) clientCreds() (id, secret string) { s.mu.Lock() defer s.mu.Unlock() if s.clientID == "" { s.clientID = uuid.New().String() s.clientSecret = uuid.New().String() } return s.clientID, s.clientSecret } func (s *testOIDCServer) code() string { s.mu.Lock() defer s.mu.Unlock() if s.code_ == "" { s.code_ = uuid.New().String() } return s.code_ } func (s *testOIDCServer) claims() map[string]interface{} { s.mu.Lock() defer s.mu.Unlock() if s.claims_ == nil { s.claims_ = make(map[string]interface{}) } return s.claims_ } func (s *testOIDCServer) setClaim(name string, v interface{}) { s.mu.Lock() defer s.mu.Unlock() if s.claims_ == nil { s.claims_ = make(map[string]interface{}) } s.claims_[name] = v } func (s *testOIDCServer) key() *rsa.PrivateKey { s.mu.Lock() defer s.mu.Unlock() if s.key_ == nil { var err error s.key_, err = rsa.GenerateKey(rand.Reader, 512) if err != nil { panic(err) } } return s.key_ } golang-github-canonical-candid-1.12.3/idp/static/000077500000000000000000000000001457263123000215405ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/static/discharge_test.go000066400000000000000000000020071457263123000250560ustar00rootroot00000000000000// Copyright 2019 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package static_test import ( "testing" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/static" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/internal/discharger" "github.com/canonical/candid/internal/identity" ) func TestInteractiveDischarge(t *testing.T) { c := qt.New(t) defer c.Done() store := candidtest.NewStore() sp := store.ServerParams() sp.IdentityProviders = []idp.IdentityProvider{ static.NewIdentityProvider(getSampleParams()), } candid := candidtest.NewServer(c, sp, map[string]identity.NewAPIHandlerFunc{ "discharger": discharger.NewAPIHandler, }) dischargeCreator := candidtest.NewDischargeCreator(candid) dischargeCreator.AssertDischarge(c, httpbakery.WebBrowserInteractor{ OpenWebBrowser: candidtest.PasswordLogin(c, "user1", "pass1"), }) } golang-github-canonical-candid-1.12.3/idp/static/static.go000066400000000000000000000157051457263123000233660ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package static contains identity providers that validate against a static list of users. // This provider is only intended for testing purposes. package static import ( "context" "net/http" "regexp" "strings" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/loggo" "gopkg.in/errgo.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) var logger = loggo.GetLogger("candid.idp.static") func init() { idp.Register("static", func(unmarshal func(interface{}) error) (idp.IdentityProvider, error) { var p Params if err := unmarshal(&p); err != nil { return nil, errgo.Notef(err, "cannot unmarshal static parameters") } if p.Name == "" { p.Name = "static" } return NewIdentityProvider(p), nil }) } type Params struct { // Name is the name that will be given to the identity provider. Name string `yaml:"name"` // Description is the description of the IDP shown to the user on // the IDP selection page. Description string `yaml:"description"` // Icon contains the URL or path of an icon. Icon string `yaml:"icon"` // Domain is the domain with which all identities created by this // identity provider will be tagged (not including the @ separator). Domain string `yaml:"domain"` // Users is the set of users that are allowed to authenticate, with their // passwords and list of groups. Users map[string]UserInfo `yaml:"users"` // Hidden is set if the IDP should be hidden from interactive // prompts. Hidden bool `yaml:"hidden"` // MatchEmailAddr is a regular expression that is used to determine if // this identity provider can be used for a particular user email. MatchEmailAddr string `yaml:"match-email-addr"` // RequireMFA indicates if this provider requires the use of MFA RequireMFA bool `yaml:"require-mfa"` } type UserInfo struct { // Password is the password for the user. Password string `yaml:"password"` // Name is the full name of the user. Name string `yaml:"name"` // Email is the user e-mail. Email string `yaml:"email"` // Groups is the list of groups the user belongs to. Groups []string `yaml:"groups"` } // NewIdentityProvider creates a new static identity provider. func NewIdentityProvider(p Params) idp.IdentityProvider { if p.Description == "" { p.Description = p.Name } if p.Icon == "" { p.Icon = "/static/images/icons/static.svg" } var matchEmailAddr *regexp.Regexp if p.MatchEmailAddr != "" { var err error matchEmailAddr, err = regexp.Compile(p.MatchEmailAddr) if err != nil { // if the email address matcher doesn't compile log the error but // carry on. A regular expression that doesn't compile also doesn't // match anything. logger.Errorf("cannot compile match-email-addr regular expression: %s", err) } } return &identityProvider{ params: p, matchEmailAddr: matchEmailAddr, } } type identityProvider struct { params Params initParams idp.InitParams matchEmailAddr *regexp.Regexp } // Name implements idp.IdentityProvider.Name. func (idp *identityProvider) Name() string { return idp.params.Name } // Domain implements idp.IdentityProvider.Domain. func (idp *identityProvider) Domain() string { return idp.params.Domain } // Description implements idp.IdentityProvider.Description. func (idp *identityProvider) Description() string { return idp.params.Description } // IconURL returns the URL of an icon for the identity provider. func (idp *identityProvider) IconURL() string { return idputil.ServiceURL(idp.initParams.Location, idp.params.Icon) } // Interactive implements idp.IdentityProvider.Interactive. func (*identityProvider) Interactive() bool { return true } // Hidden implements idp.IdentityProvider.Hidden. func (idp *identityProvider) Hidden() bool { return idp.params.Hidden } // IsForEmailAddr returns true when the identity provider should be used // to identify a user with the given email address. func (idp *identityProvider) IsForEmailAddr(addr string) bool { if idp.matchEmailAddr == nil { return false } return idp.matchEmailAddr.MatchString(addr) } // Init implements idp.IdentityProvider.Init. func (idp *identityProvider) Init(ctx context.Context, params idp.InitParams) error { idp.initParams = params return nil } // URL implements idp.IdentityProvider.URL. func (idp *identityProvider) URL(state string) string { return idputil.RedirectURL(idp.initParams.URLPrefix, "/login", state) } // SetInteraction implements idp.IdentityProvider.SetInteraction. func (idp *identityProvider) SetInteraction(ierr *httpbakery.Error, dischargeID string) { } // GetGroups implements idp.IdentityProvider.GetGroups. func (idp *identityProvider) GetGroups(ctx context.Context, identity *store.Identity) ([]string, error) { _, fulluser := identity.ProviderID.Split() username := strings.SplitN(fulluser, "@", 2)[0] if user, ok := idp.params.Users[username]; ok { groups := make([]string, len(user.Groups)) copy(groups, user.Groups) return groups, nil } return []string{}, nil } // Handle implements idp.IdentityProvider.Handle. func (idp *identityProvider) Handle(ctx context.Context, w http.ResponseWriter, req *http.Request) { var ls idputil.LoginState state := req.Form.Get("state") if err := idp.initParams.Codec.Cookie(req, idputil.LoginCookieName, state, &ls); err != nil { logger.Infof("invalid login state: %s", err) idputil.BadRequestf(w, "login failed: invalid login state") return } switch strings.TrimPrefix(req.URL.Path, idp.initParams.URLPrefix) { case "/login": idpChoice := params.IDPChoiceDetails{ Domain: idp.params.Domain, Description: idp.params.Description, Name: idp.params.Name, URL: idp.URL(req.Form.Get("state")), } id, err := idputil.HandleLoginForm(ctx, w, req, idpChoice, idp.initParams.Template, idp.loginUser) if err != nil { idp.initParams.VisitCompleter.RedirectFailure(ctx, w, req, ls.ReturnTo, ls.State, err) return } if id != nil { idp.initParams.VisitCompleter.RedirectMFA(ctx, w, req, idp.params.RequireMFA, ls.ReturnTo, ls.State, state, id) return } } } func (idp *identityProvider) loginUser(ctx context.Context, user, password string) (*store.Identity, error) { if userData, ok := idp.params.Users[user]; ok { if userData.Password == password { username := idputil.NameWithDomain(user, idp.params.Domain) id := &store.Identity{ ProviderID: store.MakeProviderIdentity(idp.params.Name, username), Username: username, Name: userData.Name, Email: userData.Email, } err := idp.initParams.Store.UpdateIdentity(ctx, id, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, }) if err != nil { return nil, errgo.Mask(err) } return id, nil } } return nil, errgo.WithCausef(nil, params.ErrUnauthorized, "authentication failed for user %q", user) } golang-github-canonical-candid-1.12.3/idp/static/static_test.go000066400000000000000000000146661457263123000244320ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package static_test import ( "context" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idptest" "github.com/canonical/candid/idp/static" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/store" ) const idpPrefix = "https://idp.example.com" type staticSuite struct { idptest *idptest.Fixture } func TestStatic(t *testing.T) { qtsuite.Run(qt.New(t), &staticSuite{}) } func (s *staticSuite) Init(c *qt.C) { s.idptest = idptest.NewFixture(c, candidtest.NewStore()) } func (s *staticSuite) setupIdp(c *qt.C, params static.Params) idp.IdentityProvider { i := static.NewIdentityProvider(params) i.Init(context.TODO(), s.idptest.InitParams(c, idpPrefix)) return i } func getSampleParams() static.Params { return static.Params{ Name: "test", Users: map[string]static.UserInfo{ "user1": static.UserInfo{ Password: "pass1", Name: "User One", Email: "user1@example.com", Groups: []string{"group1", "group2"}, }, }, } } func (s *staticSuite) TestName(c *qt.C) { idp := static.NewIdentityProvider(getSampleParams()) c.Assert(idp.Name(), qt.Equals, "test") } func (s *staticSuite) TestDomain(c *qt.C) { params := getSampleParams() params.Domain = "domain" idp := static.NewIdentityProvider(params) c.Assert(idp.Domain(), qt.Equals, "domain") } func (s *staticSuite) TestDescription(c *qt.C) { params := getSampleParams() params.Description = "test IDP description" idp := static.NewIdentityProvider(params) c.Assert(idp.Description(), qt.Equals, "test IDP description") params.Description = "" idp = static.NewIdentityProvider(params) c.Assert(idp.Description(), qt.Equals, params.Name) } func (s *staticSuite) TestIconURL(c *qt.C) { i := static.NewIdentityProvider(getSampleParams()) err := i.Init(context.Background(), idp.InitParams{ Location: "https://www.example.com/candid", }) c.Assert(err, qt.IsNil) c.Assert(i.IconURL(), qt.Equals, "https://www.example.com/candid/static/images/icons/static.svg") } func (s *staticSuite) TestAbsoluteIconURL(c *qt.C) { params := getSampleParams() params.Icon = "https://www.example.com/icon.bmp" idp := static.NewIdentityProvider(params) c.Assert(idp.IconURL(), qt.Equals, "https://www.example.com/icon.bmp") } func (s *staticSuite) TestRelativeIconURL(c *qt.C) { params := getSampleParams() params.Icon = "/static/icon.bmp" i := static.NewIdentityProvider(params) err := i.Init(context.Background(), idp.InitParams{ Location: "https://www.example.com/candid", }) c.Assert(err, qt.IsNil) c.Assert(i.IconURL(), qt.Equals, "https://www.example.com/candid/static/icon.bmp") } func (s *staticSuite) TestInteractive(c *qt.C) { idp := static.NewIdentityProvider(getSampleParams()) c.Assert(idp.Interactive(), qt.Equals, true) } func (s *staticSuite) TestHidden(c *qt.C) { idp := static.NewIdentityProvider(getSampleParams()) c.Assert(idp.Hidden(), qt.Equals, false) p := getSampleParams() p.Hidden = true idp = static.NewIdentityProvider(p) c.Assert(idp.Hidden(), qt.Equals, true) } func (s *staticSuite) TestHandle(c *qt.C) { i := s.setupIdp(c, getSampleParams()) id, err := s.idptest.DoInteractiveLogin(c, i, idpPrefix+"/login", candidtest.PostLoginForm("user1", "pass1")) c.Assert(err, qt.IsNil) candidtest.AssertEqualIdentity(c, id, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "user1"), Username: "user1", Name: "User One", Email: "user1@example.com", }) s.idptest.Store.AssertUser(c, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "user1"), Username: "user1", Name: "User One", Email: "user1@example.com", }) } func (s *staticSuite) TestHandleWithDomain(c *qt.C) { params := getSampleParams() params.Domain = "domain" i := s.setupIdp(c, params) id, err := s.idptest.DoInteractiveLogin(c, i, idpPrefix+"/login", candidtest.PostLoginForm("user1", "pass1")) c.Assert(err, qt.IsNil) candidtest.AssertEqualIdentity(c, id, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "user1@domain"), Username: "user1@domain", Name: "User One", Email: "user1@example.com", }) s.idptest.Store.AssertUser(c, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "user1@domain"), Username: "user1@domain", Name: "User One", Email: "user1@example.com", }) } func (s *staticSuite) TestGetGroups(c *qt.C) { params := getSampleParams() i := s.setupIdp(c, params) _, err := s.idptest.DoInteractiveLogin(c, i, idpPrefix+"/login", candidtest.PostLoginForm("user1", "pass1")) c.Assert(err, qt.IsNil) identity := s.idptest.Store.AssertUser(c, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "user1"), Username: "user1", Name: "User One", Email: "user1@example.com", }) groups, err := i.GetGroups(s.idptest.Ctx, identity) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"group1", "group2"}) } func (s *staticSuite) TestGetGroupsReturnsNewSlice(c *qt.C) { params := getSampleParams() params.Domain = "domain" i := s.setupIdp(c, params) _, err := s.idptest.DoInteractiveLogin(c, i, idpPrefix+"/login", candidtest.PostLoginForm("user1", "pass1")) c.Assert(err, qt.IsNil) identity := s.idptest.Store.AssertUser(c, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "user1@domain"), Username: "user1@domain", Name: "User One", Email: "user1@example.com", }) groups, err := i.GetGroups(s.idptest.Ctx, identity) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"group1", "group2"}) groups[0] = "group1@domain" groups, err = i.GetGroups(s.idptest.Ctx, identity) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"group1", "group2"}) } func (s *staticSuite) TestHandleFailedLoginWrongPassword(c *qt.C) { i := s.setupIdp(c, getSampleParams()) _, err := s.idptest.DoInteractiveLogin(c, i, idpPrefix+"/login", candidtest.PostLoginForm("user1", "wrong-pass")) c.Assert(err, qt.ErrorMatches, `authentication failed for user "user1"`) } func (s *staticSuite) TestHandleFailedLoginUnknownUser(c *qt.C) { i := s.setupIdp(c, getSampleParams()) _, err := s.idptest.DoInteractiveLogin(c, i, idpPrefix+"/login", candidtest.PostLoginForm("unknown", "pass")) c.Assert(err, qt.ErrorMatches, `authentication failed for user "unknown"`) } golang-github-canonical-candid-1.12.3/idp/usso/000077500000000000000000000000001457263123000212425ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/usso/discharge_test.go000066400000000000000000000025461457263123000245700ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package usso_test import ( "testing" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/usso" "github.com/canonical/candid/idp/usso/internal/mockusso" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/internal/discharger" "github.com/canonical/candid/internal/identity" ) func TestInteractiveDischarge(t *testing.T) { c := qt.New(t) defer c.Done() store := candidtest.NewStore() sp := store.ServerParams() sp.IdentityProviders = []idp.IdentityProvider{ usso.NewIdentityProvider(usso.Params{}), } candid := candidtest.NewServer(c, sp, map[string]identity.NewAPIHandlerFunc{ "discharger": discharger.NewAPIHandler, }) dischargeCreator := candidtest.NewDischargeCreator(candid) ussoSrv := mockusso.NewServer() defer ussoSrv.Close() ussoSrv.MockUSSO.AddUser(&mockusso.User{ ID: "test", NickName: "test", FullName: "Test User", Email: "test@example.com", Groups: []string{"test1", "test2"}, }) ussoSrv.MockUSSO.SetLoginUser("test") dischargeCreator.AssertDischarge(c, httpbakery.WebBrowserInteractor{ OpenWebBrowser: candidtest.OpenWebBrowser(c, candidtest.SelectInteractiveLogin(nil)), }) } golang-github-canonical-candid-1.12.3/idp/usso/internal/000077500000000000000000000000001457263123000230565ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/usso/internal/kvnoncestore/000077500000000000000000000000001457263123000255765ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/usso/internal/kvnoncestore/export_test.go000066400000000000000000000002211457263123000305000ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package kvnoncestore var Accept = (*Store).accept golang-github-canonical-candid-1.12.3/idp/usso/internal/kvnoncestore/store.go000066400000000000000000000044041457263123000272630ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package kvnoncestore is an openid.NonceStore that is backed by a store.KeyValueStore. package kvnoncestore import ( "context" "fmt" "time" "github.com/juju/loggo" "github.com/juju/simplekv" "gopkg.in/errgo.v1" ) var logger = loggo.GetLogger("idp.usso.internal.kvnoncestore") // Store is an openid.NonceStore that is backed by a store.KeyValueStore. type Store struct { store simplekv.Store maxAge time.Duration } // New creates a new Store. func New(store simplekv.Store, maxAge time.Duration) *Store { return &Store{ store: store, maxAge: maxAge, } } // Accept implements openid.NonceStore.Accept. func (s *Store) Accept(endpoint, nonce string) error { return s.accept(endpoint, nonce, time.Now()) } // accept is the implementation of Accept. The third parameter is the // current time, useful for testing. func (s *Store) accept(endpoint, nonce string, now time.Time) error { // From the openid specification: // // openid.response_nonce // // Value: A string 255 characters or less in length, that MUST be // unique to this particular successful authentication response. // The nonce MUST start with the current time on the server, and // MAY contain additional ASCII characters in the range 33-126 // inclusive (printable non-whitespace characters), as necessary // to make each response unique. The date and time MUST be // formatted as specified in section 5.6 of [RFC3339], with the // following restrictions: // // + All times must be in the UTC timezone, indicated with a "Z". // // + No fractional seconds are allowed // // For example: 2005-05-15T17:11:51ZUNIQUE if len(nonce) < 20 { return errgo.Newf("%q does not contain a valid timestamp", nonce) } t, err := time.Parse(time.RFC3339, nonce[:20]) if err != nil { return errgo.Notef(err, "%q does not contain a valid timestamp", nonce) } if t.Before(now.Add(-s.maxAge)) { return errgo.Newf("%q too old", nonce) } key := fmt.Sprintf("nonce#%s#%s", endpoint, nonce) err = simplekv.SetKeyOnce(context.Background(), s.store, key, nil, t.Add(s.maxAge)) if errgo.Cause(err) == simplekv.ErrDuplicateKey { return errgo.Newf("%q already seen for %q", nonce, endpoint) } return errgo.Mask(err) } golang-github-canonical-candid-1.12.3/idp/usso/internal/kvnoncestore/store_test.go000066400000000000000000000045361457263123000303300ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package kvnoncestore_test import ( "context" "testing" "time" qt "github.com/frankban/quicktest" "github.com/yohcop/openid-go" "github.com/canonical/candid/idp/usso/internal/kvnoncestore" "github.com/canonical/candid/internal/candidtest" ) var _ openid.NonceStore = (*kvnoncestore.Store)(nil) var acceptTests = []struct { about string endpoint string nonce string expectError string }{{ about: "not seen", endpoint: "https://example.com", nonce: "2014-12-25T00:00:00Z1", }, { about: "seen before", endpoint: "https://example.com", nonce: "2014-12-25T00:00:00Z0", expectError: `"2014-12-25T00:00:00Z0" already seen for "https://example.com"`, }, { about: "seen at another endpoint", endpoint: "https://example.com/2", nonce: "2014-12-25T00:00:00Z0", }, { about: "empty nonce", endpoint: "https://example.com", nonce: "", expectError: `"" does not contain a valid timestamp`, }, { about: "bad nonce", endpoint: "https://example.com", nonce: "1234", expectError: `"1234" does not contain a valid timestamp`, }, { about: "bad time", endpoint: "https://example.com", nonce: "2015/12/25 00:00:00Z1", expectError: `"2015/12/25 00:00:00Z1" does not contain a valid timestamp: parsing time "2015/12/25 00:00:00Z" as "2006-01-02T15:04:05Z07:00": cannot parse "/12/25 00:00:00Z" as "-"`, }, { about: "too old", endpoint: "https://example.com", nonce: "2014-12-24T23:58:59Z0", expectError: `"2014-12-24T23:58:59Z0" too old`, }} func TestAccept(t *testing.T) { c := qt.New(t) defer c.Done() kv, err := candidtest.NewStore().ProviderDataStore.KeyValueStore(context.Background(), "test") c.Assert(err, qt.IsNil) store := kvnoncestore.New(kv, time.Minute) now, err := time.Parse(time.RFC3339, "2014-12-25T00:00:00Z") c.Assert(err, qt.IsNil) err = kvnoncestore.Accept(store, "https://example.com", "2014-12-25T00:00:00Z0", now) c.Assert(err, qt.IsNil) for i, test := range acceptTests { c.Run(test.about, func(c *qt.C) { c.Logf("%d. %s", i, test.about) err := kvnoncestore.Accept(store, test.endpoint, test.nonce, now) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) return } c.Assert(err, qt.IsNil) }) } } golang-github-canonical-candid-1.12.3/idp/usso/internal/mockusso/000077500000000000000000000000001457263123000247215ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/usso/internal/mockusso/suite.go000066400000000000000000000017161457263123000264060ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package mockusso import ( "net/http" "net/http/httptest" "github.com/juju/qthttptest" ) // Server represents a mock USSO server. type Server struct { MockUSSO *Handler server *httptest.Server saved http.RoundTripper } // NewServer starts a mock USSO server and also modifies // http.DefaultTransport to redirect requests addressed to // https://login.ubuntu.com to it. // // The returned Server must be closed after use. func NewServer() *Server { s := &Server{ MockUSSO: New("https://login.ubuntu.com"), } s.server = httptest.NewServer(s.MockUSSO) rt := qthttptest.URLRewritingTransport{ MatchPrefix: "https://login.ubuntu.com", Replace: s.server.URL, RoundTripper: http.DefaultTransport, } s.saved = http.DefaultTransport http.DefaultTransport = rt return s } func (s *Server) Close() { http.DefaultTransport = s.saved s.server.Close() } golang-github-canonical-candid-1.12.3/idp/usso/internal/mockusso/usso.go000066400000000000000000000164251457263123000262510ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package mockusso import ( "encoding/json" "fmt" "io/ioutil" "mime" "net/http" "strings" "github.com/julienschmidt/httprouter" "github.com/mhilton/openid/openid2" ) type User struct { ID string NickName string FullName string Email string Groups []string // OAuth Credentials ConsumerSecret string TokenKey string TokenSecret string } // Handler is a http.Handler that provides a mock implementation of // Ubuntu SSO. It is designed to closely match the responses provided by // Ubuntu SSO providing openid login and oauth verification. type Handler struct { openidUser string users map[string]*User router *httprouter.Router location string excludeExtensions bool } func New(location string) *Handler { h := &Handler{ users: map[string]*User{}, router: httprouter.New(), location: location, } openidHandler := &openid2.Handler{ Login: h, } h.router.GET("/", h.root) h.router.HEAD("/", h.root) h.router.GET("/+xrds", h.xrds) h.router.GET("/+id/:id", h.id) h.router.HEAD("/+id/:id", h.id) h.router.GET("/+id/:id/+xrds", h.xrdsid) h.router.Handler("GET", "/+openid", openidHandler) h.router.Handler("POST", "/+openid", openidHandler) h.router.POST("/api/v2/requests/validate", h.validate) h.router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "could not find %s\n", req.URL) }) return h } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.router.ServeHTTP(w, r) } // Login handles OpenID login requests. func (h *Handler) Login(_ http.ResponseWriter, _ *http.Request, lr *openid2.LoginRequest) (*openid2.LoginResponse, error) { u := h.users[h.openidUser] var extensions []openid2.Extension if !h.excludeExtensions { extensions = []openid2.Extension{{ Namespace: "http://openid.net/extensions/sreg/1.1", Prefix: "sreg", Params: map[string]string{ "nickname": u.NickName, "fullname": u.FullName, "email": u.Email, }, }, { Namespace: "http://ns.launchpad.net/2007/openid-teams", Prefix: "lp", Params: map[string]string{ "is_member": strings.Join(u.Groups, ","), }, }} } return &openid2.LoginResponse{ Identity: h.location + "/+id/" + h.openidUser, ClaimedID: h.location + "/+id/" + h.openidUser, OPEndpoint: h.location + "/+openid", Extensions: extensions, }, nil } // AddUser adds u to the handles user database. func (h *Handler) AddUser(u *User) { h.users[u.ID] = u } // Reset sets all of the state in the Handler back to the default. This // should be called between tests. func (h *Handler) Reset() { h.users = map[string]*User{} h.openidUser = "" h.excludeExtensions = false } // SetLoginUser sets the user that is logged in when an OpenID request is // recieved. func (h *Handler) SetLoginUser(user string) { if _, ok := h.users[user]; !ok { panic("no such user: " + user) } h.openidUser = user } // ExcludeExtensions prevents an OpenID login response from including any // extensions. func (h *Handler) ExcludeExtensions() { h.excludeExtensions = true } func (h *Handler) root(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { w.Header().Set("Content-Type", "text/html") w.Header().Set("X-Xrds-Location", h.location+"/+xrds") content := `Mock UbuntuSSO` w.Header().Set("Content-Length", fmt.Sprint(len(content))) if r.Method == "HEAD" { return } w.Write([]byte(content)) } func (h *Handler) xrds(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { w.Header().Set("Content-Type", "application/xrds+xml") w.Write([]byte(` http://specs.openid.net/auth/2.0/server http://openid.net/srv/ax/1.0 http://openid.net/extensions/sreg/1.1 http://ns.launchpad.net/2007/openid-teams ` + h.location + `/+openid `)) } func (h *Handler) id(w http.ResponseWriter, r *http.Request, p httprouter.Params) { id := p.ByName("id") w.Header().Set("Content-Type", "text/html") w.Header().Set("X-Xrds-Location", fmt.Sprintf("%s/+id/%s/+xrds", h.location, id)) content := fmt.Sprintf(`Mock UbuntuSSO

%s

`, id) w.Header().Set("Content-Length", fmt.Sprint(len(content))) if r.Method == "HEAD" { return } w.Write([]byte(content)) } func (h *Handler) xrdsid(w http.ResponseWriter, r *http.Request, p httprouter.Params) { id := p.ByName("id") w.Header().Set("Content-Type", "application/xrds+xml") fmt.Fprintf(w, ` http://specs.openid.net/auth/2.0/signon `+h.location+`/+openid `+h.location+`+id/%s `, id) } func (h *Handler) validate(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { w.Header().Set("Content-Type", "application/json") enc := json.NewEncoder(w) var response struct { IsValid bool `json:"is_valid"` Error string `json:"error,omitempty"` } mt, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) if err != nil { response.Error = fmt.Sprintf("error parsing Content-Type: %s", err) w.WriteHeader(http.StatusInternalServerError) enc.Encode(response) return } if mt != "application/json" { response.Error = fmt.Sprintf("incorrect content type %q", r.Header.Get("Content-Type")) w.WriteHeader(http.StatusBadRequest) enc.Encode(response) return } buf, err := ioutil.ReadAll(r.Body) if err != nil { response.Error = fmt.Sprintf("error reading request: %s", err) w.WriteHeader(http.StatusInternalServerError) enc.Encode(response) return } var request struct { URL string `json:"http_url"` Method string `json:"http_method"` Authorization string `json:"authorization"` QueryString string `json:"query_string"` } if err := json.Unmarshal(buf, &request); err != nil { response.Error = fmt.Sprintf("error reading request: %s", err) w.WriteHeader(http.StatusBadRequest) enc.Encode(response) return } params := parseOAuth(request.Authorization) // For now just check the keys are ones we know about. user := h.users[params["oauth_consumer_key"]] if user == nil { enc.Encode(response) return } if user.TokenKey != params["oauth_token"] { enc.Encode(response) return } response.IsValid = true enc.Encode(response) return } // parse the OAuth Authorization header see // http://tools.ietf.org/html/rfc5849#section-3.1 func parseOAuth(oauth string) map[string]string { oauth = strings.TrimSpace(oauth) parts := strings.SplitN(oauth, " ", 2) if !strings.EqualFold(parts[0], "OAuth") { return nil } params := strings.Split(parts[1], ",") parsed := make(map[string]string, len(params)) for _, p := range params { p = strings.TrimSpace(p) parts := strings.SplitN(p, "=", 2) v := strings.TrimPrefix(parts[1], `"`) v = strings.TrimSuffix(v, `"`) parsed[parts[0]] = v } return parsed } golang-github-canonical-candid-1.12.3/idp/usso/usso.go000066400000000000000000000300311457263123000225570ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Pacakge usso is an identity provider that authenticates against Ubuntu // SSO using OpenID. package usso import ( "context" "net/http" "strings" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/loggo" "github.com/juju/names/v4" "github.com/juju/usso" "github.com/juju/usso/openid" "github.com/juju/utils/v2/cache" "github.com/prometheus/client_golang/prometheus" "gopkg.in/errgo.v1" "launchpad.net/lpad" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil" "github.com/canonical/candid/idp/usso/internal/kvnoncestore" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) var logger = loggo.GetLogger("candid.idp.usso") func init() { idp.Register("usso", func(unmarshal func(interface{}) error) (idp.IdentityProvider, error) { var p Params if err := unmarshal(&p); err != nil { return nil, errgo.Notef(err, "cannot unmarshal usso parameters") } return NewIdentityProvider(p), nil }) } type Params struct { // Name is the name that will be given to the identity provider. Name string `yaml:"name"` // Description is the description that will be used with the // identity provider. If this is not set then Name will be used. Description string `yaml:"description"` // LaunchpadTeams contains any private teams that the system needs to // know about. LaunchpadTeams []string `yaml:"launchpad-teams"` // Domain contains the domain that the identities are created in. Domain string // Icon contains the URL or path of an icon. Icon string `yaml:"icon"` // Staging enables using the staging login and launchpad servers. Staging bool // FixedUsername stops any username changes in Ubuntu SSO being // propergated into candid. Once an identity has been registered it's // Username will not be changed. FixedUsername bool `yaml:"fixed-username"` } // NewIdentityProvider creates a new LDAP identity provider. func NewIdentityProvider(p Params) idp.IdentityProvider { if p.Name == "" { p.Name = "usso" } if p.Description == "" { p.Description = "Ubuntu SSO" } if p.Icon == "" { p.Icon = "/static/images/icons/usso.svg" } return &identityProvider{ groupCache: cache.New(10 * time.Minute), groupMonitor: prometheus.NewSummary(prometheus.SummaryOpts{ Namespace: "candid", Subsystem: "launchpad", Name: "get_launchpad_groups", Help: "The duration of launchpad login, /people, and super_teams_collection_link requests.", }), params: p, } } // USSOIdentityProvider allows login using Ubuntu SSO credentials. type identityProvider struct { client *openid.Client initParams idp.InitParams groupCache *cache.Cache groupMonitor prometheus.Summary params Params } // Name gives the name of the identity provider (usso). func (idp *identityProvider) Name() string { return idp.params.Name } // Domain implements idp.IdentityProvider.Domain. func (idp *identityProvider) Domain() string { return idp.params.Domain } // Description gives a description of the identity provider. func (idp *identityProvider) Description() string { return idp.params.Description } // IconURL returns the URL of an icon for the identity provider. func (idp *identityProvider) IconURL() string { return idputil.ServiceURL(idp.initParams.Location, idp.params.Icon) } // Interactive specifies that this identity provider is interactive. func (*identityProvider) Interactive() bool { return true } // Hidden implements idp.IdentityProvider.Hidden. func (*identityProvider) Hidden() bool { return false } // Init initialises this identity provider func (idp *identityProvider) Init(_ context.Context, params idp.InitParams) error { idp.initParams = params srv := usso.ProductionUbuntuSSOServer if idp.params.Staging { srv = usso.StagingUbuntuSSOServer } idp.client = openid.NewClient( srv, kvnoncestore.New(params.KeyValueStore, time.Minute), nil, ) return nil } // URL gets the login URL to use this identity provider. func (idp *identityProvider) URL(state string) string { return idputil.RedirectURL(idp.initParams.URLPrefix, "/login", state) } // SetInteraction sets the interaction information for func (idp *identityProvider) SetInteraction(ierr *httpbakery.Error, dischargeID string) { } // Handle handles the Ubuntu SSO login process. func (idp *identityProvider) Handle(ctx context.Context, w http.ResponseWriter, req *http.Request) { switch req.URL.Path { case "/callback": idp.callback(ctx, w, req) default: idp.login(ctx, w, req) } } func (idp *identityProvider) login(ctx context.Context, w http.ResponseWriter, req *http.Request) { query := "?state=" + idputil.State(req) realm := idp.initParams.URLPrefix + "/callback" callback := realm + query url := idp.client.RedirectURL(&openid.Request{ ReturnTo: callback, Realm: realm, Teams: idp.params.LaunchpadTeams, SRegRequired: []string{openid.SRegEmail, openid.SRegFullName, openid.SRegNickname}, }) http.Redirect(w, req, url, http.StatusFound) } func (idp *identityProvider) callback(ctx context.Context, w http.ResponseWriter, req *http.Request) { var ls idputil.LoginState if err := idp.initParams.Codec.Cookie(req, idputil.LoginCookieName, req.Form.Get("state"), &ls); err != nil { logger.Infof("Invalid login state: %s", err) idputil.BadRequestf(w, "Login failed: invalid login state") return } successf := func(id *store.Identity) { idp.initParams.VisitCompleter.RedirectSuccess(ctx, w, req, ls.ReturnTo, ls.State, id) } errorf := func(err error) { idp.initParams.VisitCompleter.RedirectFailure(ctx, w, req, ls.ReturnTo, ls.State, err) } resp, err := idp.client.Verify(idp.initParams.URLPrefix + req.URL.String()) if err != nil { errorf(err) return } // Work around bug in the usso package if len(resp.Teams) == 1 && resp.Teams[0] == "" { resp.Teams = nil } var username string if resp.SReg[openid.SRegNickname] != "" { username = idputil.NameWithDomain(resp.SReg[openid.SRegNickname], idp.params.Domain) } email := resp.SReg[openid.SRegEmail] fullname := resp.SReg[openid.SRegFullName] var providerInfo map[string][]string if len(resp.Teams) > 0 { providerInfo = map[string][]string{ "groups": resp.Teams, } } identity := store.Identity{ ProviderID: store.MakeProviderIdentity(idp.Name(), resp.ID), } err = idp.initParams.Store.Identity(ctx, &identity) if errgo.Cause(err) == store.ErrNotFound { // If the identity is not found this is the first time they have // logged in, attempt to create an identity for them. identity.Username = username identity.Email = email identity.Name = fullname identity.ProviderInfo = providerInfo err = idp.registerIdentity(ctx, &identity) if errgo.Cause(err) == store.ErrDuplicateUsername { // TODO(mhilton) format an error with useful instructions. logger.Errorf("Error creating identity for %s, duplicate username %s", identity.ProviderID, username) err = errgo.WithCausef(nil, params.ErrorCode("bad username"), "cannot create identity with username %s", username) } } if err != nil { errorf(err) return } var doUpdate bool var update store.Update updateIdentity := identity if !idp.params.FixedUsername && username != "" && username != identity.Username { // TODO(mhilton): candid should have a service wide policy for // username validity. if names.IsValidUser(username) { doUpdate = true updateIdentity.Username = username update[store.Username] = store.Set } else { logger.Warningf("not updating username for %s to invalid username %q", identity.ProviderID, username) } } if email != "" && email != identity.Email { doUpdate = true updateIdentity.Email = email update[store.Email] = store.Set } if fullname != "" && fullname != identity.Name { doUpdate = true updateIdentity.Name = fullname update[store.Name] = store.Set } if len(providerInfo) != 0 && !valuesMatch(providerInfo["groups"], identity.ProviderInfo["groups"]) { doUpdate = true updateIdentity.ProviderInfo = providerInfo update[store.ProviderInfo] = store.Set } if doUpdate { if err := idp.initParams.Store.UpdateIdentity(ctx, &updateIdentity, update); err != nil { // If the identity information cannot be updated, just stick // with the old data. We know who the identity is. logger.Errorf("cannot update indentity information for %s: %v", updateIdentity.ProviderID, err) } else { identity = updateIdentity } } successf(&identity) } func (idp *identityProvider) registerIdentity(ctx context.Context, identity *store.Identity) error { switch { case identity.Username == "": return errgo.New("username not specified") case !names.IsValidUser(identity.Username): return errgo.WithCausef(nil, params.ErrorCode("bad username"), "invalid username %q", identity.Username) case identity.Email == "": return errgo.New("email address not specified") case identity.Name == "": return errgo.New("full name not specified") } err := idp.initParams.Store.UpdateIdentity(ctx, identity, store.Update{ store.Username: store.Set, store.Email: store.Set, store.Name: store.Set, store.ProviderInfo: store.Set, }) return errgo.Mask(err, errgo.Is(store.ErrDuplicateUsername)) } // GetGroups implements idp.IdentityProvider.GetGroups by fetching group // information from launchpad. func (idp *identityProvider) GetGroups(_ context.Context, id *store.Identity) ([]string, error) { _, ussoID := id.ProviderID.Split() groups0, err := idp.groupCache.Get(ussoID, func() (interface{}, error) { t := time.Now() groups, err := idp.getLaunchpadGroupsNoCache(ussoID) idp.groupMonitor.Observe(float64(time.Since(t)) / float64(time.Microsecond)) return groups, err }) if err != nil { return nil, errgo.Mask(err) } groups := groups0.([]string) privateGroups := id.ProviderInfo["groups"] allGroups := make([]string, len(groups)+len(privateGroups)) copy(allGroups, groups) copy(allGroups[len(groups):], privateGroups) return allGroups, nil } // getLaunchpadGroups tries to fetch the list of teams the user // belongs to in launchpad. Only public teams are supported. func (idp *identityProvider) getLaunchpadGroupsNoCache(ussoID string) ([]string, error) { srv := lpad.Production if idp.params.Staging { srv = lpad.Staging } root, err := lpad.Login(srv, &lpad.OAuth{Consumer: "idm", Anonymous: true}) if err != nil { return nil, errgo.Notef(err, "cannot connect to launchpad") } user, err := idp.getLaunchpadPersonByOpenID(root, ussoID) if err != nil { return nil, errgo.Notef(err, "cannot find user %s", ussoID) } teams, err := user.Link("super_teams_collection_link").Get(nil) if err != nil { return nil, errgo.Notef(err, "cannot get team list for launchpad user %q", user.Name()) } groups := make([]string, 0, teams.TotalSize()) teams.For(func(team *lpad.Value) error { groups = append(groups, team.StringField("name")) return nil }) return groups, nil } func (idp *identityProvider) getLaunchpadPersonByOpenID(root *lpad.Root, ussoID string) (*lpad.Person, error) { lpPrefix := "https://login.launchpad.net/+id/" ussoPrefix := "https://login.ubuntu.com/+id/" if idp.params.Staging { lpPrefix = "https://login-lp.staging.ubuntu.com/+id/" ussoPrefix = "https://login.staging.ubuntu.com/+id/" } launchpadID := lpPrefix + strings.TrimPrefix(ussoID, ussoPrefix) v, err := root.Location("/people").Get(lpad.Params{"ws.op": "getByOpenIDIdentifier", "identifier": launchpadID}) // TODO if err == lpad.ErrNotFound, return a not found error // so that we won't round-trip to launchpad for users that don't exist there. if err != nil { return nil, errgo.Notef(err, "cannot find user %s", ussoID) } return &lpad.Person{v}, nil } // valuesMatch determines if string sets a and b contain exactly the same // values. func valuesMatch(a, b []string) bool { // filter out any repeated values in a inA := make(map[string]bool, len(a)) for _, v := range a { inA[v] = true } inB := make(map[string]bool, len(b)) for _, v := range b { inB[v] = true } if len(inA) != len(inB) { return false } for k := range inA { if !inB[k] { return false } } return true } golang-github-canonical-candid-1.12.3/idp/usso/usso_test.go000066400000000000000000000443701457263123000236310ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package usso_test import ( "context" "fmt" "net/http" "net/http/httptest" "net/url" "testing" "time" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/juju/qthttptest" "gopkg.in/yaml.v2" "github.com/canonical/candid/config" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idptest" "github.com/canonical/candid/idp/idputil" "github.com/canonical/candid/idp/usso" "github.com/canonical/candid/idp/usso/internal/mockusso" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/store" ) type ussoSuite struct { idptest *idptest.Fixture idp idp.IdentityProvider } func TestUSSO(t *testing.T) { qtsuite.Run(qt.New(t), &ussoSuite{}) } const idpPrefix = "http://idp.example.com" func (s *ussoSuite) Init(c *qt.C) { s.idptest = idptest.NewFixture(c, candidtest.NewStore()) s.idp = usso.NewIdentityProvider(usso.Params{}) err := s.idp.Init(s.idptest.Ctx, s.idptest.InitParams(c, idpPrefix)) c.Assert(err, qt.IsNil) } func (s *ussoSuite) TestConfig(c *qt.C) { configYaml := ` identity-providers: - type: usso - type: usso name: usso2 description: Another USSO staging: true ` var conf config.Config err := yaml.Unmarshal([]byte(configYaml), &conf) c.Assert(err, qt.IsNil) c.Assert(conf.IdentityProviders, qt.HasLen, 2) c.Assert(conf.IdentityProviders[0].Name(), qt.Equals, "usso") c.Assert(conf.IdentityProviders[0].Description(), qt.Equals, "Ubuntu SSO") c.Assert(conf.IdentityProviders[1].Name(), qt.Equals, "usso2") c.Assert(conf.IdentityProviders[1].Description(), qt.Equals, "Another USSO") } func (s *ussoSuite) TestName(c *qt.C) { c.Assert(s.idp.Name(), qt.Equals, "usso") } func (s *ussoSuite) TestDomain(c *qt.C) { c.Assert(s.idp.Domain(), qt.Equals, "") } func (s *ussoSuite) TestDescription(c *qt.C) { c.Assert(s.idp.Description(), qt.Equals, "Ubuntu SSO") } func (s *ussoSuite) TestIconURL(c *qt.C) { idp := usso.NewIdentityProvider(usso.Params{}) params := s.idptest.InitParams(c, idpPrefix) params.Location = "https://www.example.com/candid" err := idp.Init(s.idptest.Ctx, params) c.Assert(err, qt.IsNil) c.Assert(idp.IconURL(), qt.Equals, "https://www.example.com/candid/static/images/icons/usso.svg") } func (s *ussoSuite) TestAbsoluteIconURL(c *qt.C) { idp := usso.NewIdentityProvider(usso.Params{ Icon: "https://www.example.com/icon.bmp", }) err := idp.Init(s.idptest.Ctx, s.idptest.InitParams(c, idpPrefix)) c.Assert(err, qt.IsNil) c.Assert(idp.IconURL(), qt.Equals, "https://www.example.com/icon.bmp") } func (s *ussoSuite) TestRelativeIconURL(c *qt.C) { idp := usso.NewIdentityProvider(usso.Params{ Icon: "/static/icon.bmp", }) params := s.idptest.InitParams(c, idpPrefix) params.Location = "https://www.example.com/candid" err := idp.Init(s.idptest.Ctx, params) c.Assert(err, qt.IsNil) c.Assert(idp.IconURL(), qt.Equals, "https://www.example.com/candid/static/icon.bmp") } func (s *ussoSuite) TestInteractive(c *qt.C) { c.Assert(s.idp.Interactive(), qt.Equals, true) } func (s *ussoSuite) TestHidden(c *qt.C) { c.Assert(s.idp.Hidden(), qt.Equals, false) } func (s *ussoSuite) TestURL(c *qt.C) { c.Assert(s.idp.URL("1"), qt.Equals, "http://idp.example.com/login?state=1") } func (s *ussoSuite) TestRedirect(c *qt.C) { u := s.getRedirectURL(c, "/login") c.Assert(u.Host, qt.Equals, "login.ubuntu.com") c.Assert(u.Path, qt.Equals, "/+openid") q := u.Query() c.Assert(q.Get("openid.return_to"), qt.Matches, "http://idp.example.com/callback\\?state=[-_0-9A-Za-z]+") delete(q, "openid.return_to") c.Assert(q, qt.DeepEquals, url.Values{ "openid.ns": []string{"http://specs.openid.net/auth/2.0"}, "openid.claimed_id": []string{"http://specs.openid.net/auth/2.0/identifier_select"}, "openid.identity": []string{"http://specs.openid.net/auth/2.0/identifier_select"}, "openid.mode": []string{"checkid_setup"}, "openid.realm": []string{"http://idp.example.com/callback"}, "openid.ns.sreg": []string{"http://openid.net/extensions/sreg/1.1"}, "openid.sreg.required": []string{"email,fullname,nickname"}, }) } func (s *ussoSuite) TestRedirectWithLaunchpadTeams(c *qt.C) { s.idp = usso.NewIdentityProvider(usso.Params{LaunchpadTeams: []string{"myteam1", "myteam2"}}) err := s.idp.Init(s.idptest.Ctx, s.idptest.InitParams(c, "http://idp.example.com")) c.Assert(err, qt.IsNil) u := s.getRedirectURL(c, "/login") c.Assert(u.Host, qt.Equals, "login.ubuntu.com") c.Assert(u.Path, qt.Equals, "/+openid") q := u.Query() c.Assert(q.Get("openid.return_to"), qt.Matches, "http://idp.example.com/callback\\?state=[-_0-9A-Za-z]+") delete(q, "openid.return_to") c.Assert(q, qt.DeepEquals, url.Values{ "openid.ns": []string{"http://specs.openid.net/auth/2.0"}, "openid.claimed_id": []string{"http://specs.openid.net/auth/2.0/identifier_select"}, "openid.identity": []string{"http://specs.openid.net/auth/2.0/identifier_select"}, "openid.mode": []string{"checkid_setup"}, "openid.realm": []string{"http://idp.example.com/callback"}, "openid.ns.lp": []string{"http://ns.launchpad.net/2007/openid-teams"}, "openid.lp.query_membership": []string{"myteam1,myteam2"}, "openid.ns.sreg": []string{"http://openid.net/extensions/sreg/1.1"}, "openid.sreg.required": []string{"email,fullname,nickname"}, }) } func (s *ussoSuite) getRedirectURL(c *qt.C, path string) *url.URL { client := idptest.NewClient(s.idp, s.idptest.Codec) client.SetLoginState(idputil.LoginState{ ReturnTo: "http://result.example.com", State: "1234", Expires: time.Now().Add(10 * time.Minute), }) resp, err := client.Get("/login") c.Assert(err, qt.IsNil) defer resp.Body.Close() c.Assert(resp.StatusCode, qt.Equals, http.StatusFound) u, err := url.Parse(resp.Header.Get("Location")) c.Assert(err, qt.IsNil) return u } func (s *ussoSuite) TestHandleSuccess(c *qt.C) { ussoSrv := mockusso.NewServer() defer ussoSrv.Close() ussoSrv.MockUSSO.AddUser(&mockusso.User{ ID: "test", NickName: "test", FullName: "Test User", Email: "test@example.com", }) ussoSrv.MockUSSO.SetLoginUser("test") id, err := s.idptest.DoInteractiveLogin(c, s.idp, idpPrefix+"/login", nil) c.Assert(err, qt.IsNil) candidtest.AssertEqualIdentity(c, id, &store.Identity{ ProviderID: "usso:https://login.ubuntu.com/+id/test", Username: "test", Name: "Test User", Email: "test@example.com", }) } func (s *ussoSuite) TestHandleSuccessNoExtensions(c *qt.C) { ussoSrv := mockusso.NewServer() defer ussoSrv.Close() err := s.idptest.Store.Store.UpdateIdentity( s.idptest.Ctx, &store.Identity{ ProviderID: store.MakeProviderIdentity("usso", "https://login.ubuntu.com/+id/test"), Username: "test", Name: "Test User", Email: "test@example.com", }, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, }, ) c.Assert(err, qt.IsNil) ussoSrv.MockUSSO.AddUser(&mockusso.User{ ID: "test", NickName: "test", FullName: "Test User", Email: "test@example.com", }) ussoSrv.MockUSSO.SetLoginUser("test") ussoSrv.MockUSSO.ExcludeExtensions() id, err := s.idptest.DoInteractiveLogin(c, s.idp, idpPrefix+"/login", nil) c.Assert(err, qt.IsNil) candidtest.AssertEqualIdentity(c, id, &store.Identity{ ProviderID: "usso:https://login.ubuntu.com/+id/test", Username: "test", Name: "Test User", Email: "test@example.com", }) } func (s *ussoSuite) TestHandleNoExtensionsNotFound(c *qt.C) { ussoSrv := mockusso.NewServer() defer ussoSrv.Close() ussoSrv.MockUSSO.AddUser(&mockusso.User{ ID: "test", NickName: "test", FullName: "Test User", Email: "test@example.com", }) ussoSrv.MockUSSO.SetLoginUser("test") ussoSrv.MockUSSO.ExcludeExtensions() id, err := s.idptest.DoInteractiveLogin(c, s.idp, idpPrefix+"/login", nil) c.Assert(err, qt.ErrorMatches, `username not specified`) c.Assert(id, qt.IsNil) } func (s *ussoSuite) TestInteractiveLoginFromDifferentProvider(c *qt.C) { mockUSSO := mockusso.New("https://badplace.example.com") server := httptest.NewServer(mockUSSO) defer server.Close() c.Patch(&http.DefaultTransport, qthttptest.URLRewritingTransport{ MatchPrefix: "https://badplace.example.com", Replace: server.URL, RoundTripper: http.DefaultTransport, }) mockUSSO.AddUser(&mockusso.User{ ID: "test", NickName: "test", FullName: "Test User", Email: "test@example.com", Groups: []string{"test1", "test2"}, }) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { req.ParseForm() s.idp.Handle(req.Context(), w, req) })) defer srv.Close() client := s.idptest.Client(c, idpPrefix, srv.URL, "https://result.example.com") cookie, state := s.idptest.LoginState(c, idputil.LoginState{ ReturnTo: "https://result.example.com", State: "1234", Expires: time.Now().Add(10 * time.Minute), }) u, err := url.Parse(idpPrefix) c.Assert(err, qt.IsNil) client.Jar.SetCookies(u, []*http.Cookie{cookie}) mockUSSO.SetLoginUser("test") v := url.Values{} v.Set("openid.ns", "http://specs.openid.net/auth/2.0") v.Set("openid.mode", "checkid_setup") v.Set("openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select") v.Set("openid.identity", "http://specs.openid.net/auth/2.0/identifier_select") v.Set("openid.return_to", idpPrefix+"/callback?state="+state) v.Set("openid.realm", idpPrefix+"/callback") u = &url.URL{ Scheme: "https", Host: "badplace.example.com", Path: "/+openid", RawQuery: v.Encode(), } resp, err := client.Get(u.String()) c.Assert(err, qt.IsNil) defer resp.Body.Close() id, err := s.idptest.ParseResponse(c, resp) c.Assert(err, qt.ErrorMatches, `OpenID response from unexpected endpoint "https://badplace.example.com/\+openid"`) c.Assert(id, qt.IsNil) } func (s *ussoSuite) TestHandleRegisterUserError(c *qt.C) { ussoSrv := mockusso.NewServer() defer ussoSrv.Close() ussoSrv.MockUSSO.AddUser(&mockusso.User{ ID: "test", NickName: "test-", FullName: "Test User", Email: "test@example.com", }) ussoSrv.MockUSSO.SetLoginUser("test") id, err := s.idptest.DoInteractiveLogin(c, s.idp, idpPrefix+"/login", nil) c.Assert(err, qt.ErrorMatches, `invalid username "test-"`) c.Assert(id, qt.IsNil) } func (s *ussoSuite) TestInvalidCookie(c *qt.C) { client := idptest.NewClient(s.idp, s.idptest.Codec) resp, err := client.Get("/callback") c.Assert(err, qt.IsNil) defer resp.Body.Close() c.Assert(resp.StatusCode, qt.Equals, http.StatusBadRequest) } func (s *ussoSuite) TestGetGroups(c *qt.C) { lp := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c.Logf("path: %s", r.URL.Path) switch r.URL.Path { case "/people": r.ParseForm() c.Check(r.Form.Get("ws.op"), qt.Equals, "getByOpenIDIdentifier") c.Check(r.Form.Get("identifier"), qt.Equals, "https://login.launchpad.net/+id/test") w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"name": "test", "super_teams_collection_link": "https://api.launchpad.net/devel/test/super_teams"}`) case "/test/super_teams": w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"total_size":3,"start":0,"entries": [{"name": "test1"},{"name":"test2"}]}`) } })) defer lp.Close() rt := qthttptest.URLRewritingTransport{ MatchPrefix: "https://api.launchpad.net/devel", Replace: lp.URL, RoundTripper: http.DefaultTransport, } savedTransport := http.DefaultTransport defer func() { http.DefaultTransport = savedTransport }() http.DefaultTransport = rt groups, err := s.idp.GetGroups(context.Background(), &store.Identity{ ProviderID: store.MakeProviderIdentity("usso", "https://login.ubuntu.com/+id/test"), }) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"test1", "test2"}) } func (s *ussoSuite) TestGetGroupsReturnsNewSlice(c *qt.C) { lp := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c.Logf("path: %s", r.URL.Path) switch r.URL.Path { case "/people": r.ParseForm() c.Check(r.Form.Get("ws.op"), qt.Equals, "getByOpenIDIdentifier") c.Check(r.Form.Get("identifier"), qt.Equals, "https://login.launchpad.net/+id/test") w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"name": "test", "super_teams_collection_link": "https://api.launchpad.net/devel/test/super_teams"}`) case "/test/super_teams": w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"total_size":3,"start":0,"entries": [{"name": "test1"},{"name":"test2"}]}`) } })) defer lp.Close() rt := qthttptest.URLRewritingTransport{ MatchPrefix: "https://api.launchpad.net/devel", Replace: lp.URL, RoundTripper: http.DefaultTransport, } savedTransport := http.DefaultTransport defer func() { http.DefaultTransport = savedTransport }() http.DefaultTransport = rt groups, err := s.idp.GetGroups(context.Background(), &store.Identity{ ProviderID: store.MakeProviderIdentity("usso", "https://login.ubuntu.com/+id/test"), }) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"test1", "test2"}) groups[0] = "test1@domain" groups, err = s.idp.GetGroups(s.idptest.Ctx, &store.Identity{ ProviderID: store.MakeProviderIdentity("usso", "https://login.ubuntu.com/+id/test"), }) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"test1", "test2"}) } func (s *ussoSuite) TestWithDomain(c *qt.C) { s.idp = usso.NewIdentityProvider(usso.Params{ Domain: "test1", }) err := s.idp.Init(s.idptest.Ctx, s.idptest.InitParams(c, idpPrefix)) c.Assert(err, qt.IsNil) c.Assert(s.idp.Domain(), qt.Equals, "test1") ussoSrv := mockusso.NewServer() defer ussoSrv.Close() ussoSrv.MockUSSO.AddUser(&mockusso.User{ ID: "test", NickName: "test", FullName: "Test User", Email: "test@example.com", }) ussoSrv.MockUSSO.SetLoginUser("test") id, err := s.idptest.DoInteractiveLogin(c, s.idp, idpPrefix+"/login", nil) c.Assert(err, qt.IsNil) candidtest.AssertEqualIdentity(c, id, &store.Identity{ ProviderID: "usso:https://login.ubuntu.com/+id/test", Username: "test@test1", Name: "Test User", Email: "test@example.com", }) } func (s *ussoSuite) TestUpdateIdentity(c *qt.C) { ussoSrv := mockusso.NewServer() defer ussoSrv.Close() ussoSrv.MockUSSO.AddUser(&mockusso.User{ ID: "test", NickName: "test", FullName: "Test User", Email: "test@example.com", Groups: []string{"test1", "test2"}, }) ussoSrv.MockUSSO.SetLoginUser("test") id, err := s.idptest.DoInteractiveLogin(c, s.idp, idpPrefix+"/login", nil) c.Assert(err, qt.IsNil) candidtest.AssertEqualIdentity(c, id, &store.Identity{ ProviderID: "usso:https://login.ubuntu.com/+id/test", Username: "test", Name: "Test User", Email: "test@example.com", ProviderInfo: map[string][]string{ "groups": []string{"test1", "test2"}, }, }) ussoSrv.MockUSSO.AddUser(&mockusso.User{ ID: "test", NickName: "test-changed", FullName: "Test User Changed", Email: "test-changed@example.com", Groups: []string{"test1", "test3"}, }) id, err = s.idptest.DoInteractiveLogin(c, s.idp, idpPrefix+"/login", nil) c.Assert(err, qt.IsNil) candidtest.AssertEqualIdentity(c, id, &store.Identity{ ProviderID: "usso:https://login.ubuntu.com/+id/test", Username: "test-changed", Name: "Test User Changed", Email: "test-changed@example.com", ProviderInfo: map[string][]string{ "groups": []string{"test1", "test3"}, }, }) } func (s *ussoSuite) TestUpdateIdentityKeepsUsernameIfNewNameInvalid(c *qt.C) { ussoSrv := mockusso.NewServer() defer ussoSrv.Close() ussoSrv.MockUSSO.AddUser(&mockusso.User{ ID: "test", NickName: "test", FullName: "Test User", Email: "test@example.com", Groups: []string{"test1", "test2"}, }) ussoSrv.MockUSSO.SetLoginUser("test") id, err := s.idptest.DoInteractiveLogin(c, s.idp, idpPrefix+"/login", nil) c.Assert(err, qt.IsNil) candidtest.AssertEqualIdentity(c, id, &store.Identity{ ProviderID: "usso:https://login.ubuntu.com/+id/test", Username: "test", Name: "Test User", Email: "test@example.com", ProviderInfo: map[string][]string{ "groups": []string{"test1", "test2"}, }, }) ussoSrv.MockUSSO.AddUser(&mockusso.User{ ID: "test", NickName: "test-changed-", FullName: "Test User Changed", Email: "test-changed@example.com", Groups: []string{"test1", "test3"}, }) id, err = s.idptest.DoInteractiveLogin(c, s.idp, idpPrefix+"/login", nil) c.Assert(err, qt.IsNil) candidtest.AssertEqualIdentity(c, id, &store.Identity{ ProviderID: "usso:https://login.ubuntu.com/+id/test", Username: "test", Name: "Test User Changed", Email: "test-changed@example.com", ProviderInfo: map[string][]string{ "groups": []string{"test1", "test3"}, }, }) } func (s *ussoSuite) TestUpdateIdentityKeepsUsernameIfFixed(c *qt.C) { idp := usso.NewIdentityProvider(usso.Params{ FixedUsername: true, }) err := idp.Init(s.idptest.Ctx, s.idptest.InitParams(c, idpPrefix)) c.Assert(err, qt.IsNil) ussoSrv := mockusso.NewServer() defer ussoSrv.Close() ussoSrv.MockUSSO.AddUser(&mockusso.User{ ID: "test", NickName: "test", FullName: "Test User", Email: "test@example.com", Groups: []string{"test1", "test2"}, }) ussoSrv.MockUSSO.SetLoginUser("test") id, err := s.idptest.DoInteractiveLogin(c, idp, idpPrefix+"/login", nil) c.Assert(err, qt.IsNil) candidtest.AssertEqualIdentity(c, id, &store.Identity{ ProviderID: "usso:https://login.ubuntu.com/+id/test", Username: "test", Name: "Test User", Email: "test@example.com", ProviderInfo: map[string][]string{ "groups": []string{"test1", "test2"}, }, }) ussoSrv.MockUSSO.AddUser(&mockusso.User{ ID: "test", NickName: "test-changed", FullName: "Test User Changed", Email: "test-changed@example.com", Groups: []string{"test1", "test3"}, }) id, err = s.idptest.DoInteractiveLogin(c, idp, idpPrefix+"/login", nil) c.Assert(err, qt.IsNil) candidtest.AssertEqualIdentity(c, id, &store.Identity{ ProviderID: "usso:https://login.ubuntu.com/+id/test", Username: "test", Name: "Test User Changed", Email: "test-changed@example.com", ProviderInfo: map[string][]string{ "groups": []string{"test1", "test3"}, }, }) } golang-github-canonical-candid-1.12.3/idp/usso/ussodischarge/000077500000000000000000000000001457263123000241055ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/usso/ussodischarge/cmd/000077500000000000000000000000001457263123000246505ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/usso/ussodischarge/cmd/login/000077500000000000000000000000001457263123000257605ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/usso/ussodischarge/cmd/login/main.go000066400000000000000000000052061457263123000272360ustar00rootroot00000000000000// login is a simple tool that can be used to test the Ubuntu SSO // discharge login protocol. package main import ( "context" "flag" "fmt" "log" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" errgo "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "gopkg.in/macaroon.v2" "github.com/canonical/candid/candidclient" "github.com/canonical/candid/candidclient/ussodischarge" ) var ( email = flag.String("email", "", "email") insecure = flag.Bool("insecure", false, "get public key over insecure connections") password = flag.String("password", "", "password") otp = flag.String("otp", "", "verification code") url = flag.String("url", "https://api.jujucharms.com/identity", "identity url") ) func main() { ctx := context.Background() log.SetFlags(log.Flags() | log.Llongfile) flag.Parse() tpl := httpbakery.NewThirdPartyLocator(nil, nil) if *insecure { tpl.AllowInsecure() } client := httpbakery.NewClient() iclient, err := candidclient.New(candidclient.NewParams{ BaseURL: *url, Client: client, }) if err != nil { log.Fatal(err) } key, err := bakery.GenerateKey() if err != nil { log.Fatal(err) } b := identchecker.NewBakery(identchecker.BakeryParams{ Location: "test", Locator: tpl, Key: key, IdentityClient: iclient, }) m, err := b.Oven.NewMacaroon(ctx, bakery.LatestVersion, []checkers.Caveat{{ Condition: "is-authenticated-user", Location: *url, }, checkers.TimeBeforeCaveat(time.Now().Add(time.Minute))}, identchecker.LoginOp) if err != nil { log.Fatalf("cannot make macaroon: %s", err) } client.AddInteractor(ussodischarge.NewInteractor(func(client *httpbakery.Client, url string) (macaroon.Slice, error) { return login(ctx, client, url) })) ms, err := client.DischargeAll(ctx, m) if err != nil { log.Fatalf("cannot discharge macaroon: %s", err) } authInfo, err := b.Checker.Auth(ms).Allow(ctx, identchecker.LoginOp) if err != nil { log.Fatalf("invalid macaroon discharge: %s", err) } fmt.Printf("success as %v\n", authInfo.Identity.Id()) } func login(ctx context.Context, doer httprequest.Doer, url string) (macaroon.Slice, error) { m, err := ussodischarge.Macaroon(ctx, doer, url) if err != nil { return nil, errgo.Mask(err) } d := &ussodischarge.Discharger{ Email: *email, Password: *password, OTP: *otp, Doer: doer, } ms, err := d.DischargeAll(ctx, m) if err != nil { return nil, errgo.Mask(err) } return ms, nil } golang-github-canonical-candid-1.12.3/idp/usso/ussodischarge/export_test.go000066400000000000000000000006271457263123000270210ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Pacakge ussomacaroon is an identity provider that authenticates against // Ubuntu SSO using Ubuntu SSO's macaroon protocol. package ussodischarge const ( OperationName = operationName TimeFormat = timeFormat ) var USSOLoginOp = ussoLoginOp type ( USSOCaveatID ussoCaveatID AccountInfo accountInfo ) golang-github-canonical-candid-1.12.3/idp/usso/ussodischarge/ussodischarge.go000066400000000000000000000306211457263123000273010ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Pacakge ussodischarge is an identity provider that authenticates against // Ubuntu SSO using Ubuntu SSO's macaroon protocol. package ussodischarge import ( "context" "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "net/http" "net/url" "strings" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/loggo" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/canonical/candid/candidclient/ussodischarge" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) var logger = loggo.GetLogger("candid.idp.usso.ussodischarge") const ( operationName = "usso-discharge-login" timeFormat = "2006-01-02T15:04:05.000000" ussoMacaroonDuration = 100 * 365 * 24 * time.Hour ) var ussoLoginOp = bakery.Op{ Entity: "ussologin", Action: "login", } func init() { idp.Register("usso_macaroon", func(unmarshal func(interface{}) error) (idp.IdentityProvider, error) { var p Params if err := unmarshal(&p); err != nil { return nil, err } return NewIdentityProvider(p) }) } // Params holds the parameters to use with UbuntuSSO macaroon identity // providers. type Params struct { // Domain will be appended to any usernames or groups provided by // the identity provider. A user created by this identity provide // will be openid@domain. Domain string `yaml:"domain"` // URL is the address of the Ubuntu SSO server. URL string `yaml:"url"` // PublicKey is the RSA public key used to encrypt caveats for // UbuntuSSO third party caveats. PublicKey PublicKey `yaml:"public-key"` } // PublicKey is a PublicKey parameter for type PublicKey struct { rsa.PublicKey } // UnmarshalText implements encoding.TextUnmarshaler by // unmarshaling the PEM-encoded RSA public key from text. func (k *PublicKey) UnmarshalText(text []byte) error { block, _ := pem.Decode(text) if block.Type != "PUBLIC KEY" { return errgo.Newf("value is not a PUBLIC KEY") } pk, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return errgo.Notef(err, "cannot parse public key") } rsapk, ok := pk.(*rsa.PublicKey) if !ok { return errgo.Newf("unsupported public key type %T", pk) } k.PublicKey = *rsapk return nil } // NewIdentityProvider creates an idp.IdentityProvider that uses Ubuntu // SSO macaroon authentication, with the configuration defined by p. func NewIdentityProvider(p Params) (idp.IdentityProvider, error) { if p.Domain == "" { return nil, errgo.New(`required parameter "domain" not specified`) } if p.URL == "" { return nil, errgo.New(`required parameter "url" not specified`) } u, err := url.Parse(p.URL) if err != nil { return nil, errgo.Notef(err, `cannot parse "url"`) } return &identityProvider{ hostname: u.Host, params: p, }, nil } // identityProvider is an identity provider that authenticates to Ubuntu // SSO by requiring the client to discharge a macaroon addressed directly // to UbuntuSSO. type identityProvider struct { hostname string params Params initParams idp.InitParams } // Name gives the name of the identity provider (usso). func (*identityProvider) Name() string { return "usso_macaroon" } // Domain implements idp.IdentityProvider.Domain func (idp *identityProvider) Domain() string { return idp.params.Domain } // Description gives a description of the identity provider. func (*identityProvider) Description() string { return "Ubuntu SSO macaroon discharge authentication" } // IconURL returns the URL of an icon for the identity provider. func (*identityProvider) IconURL() string { return "" } // Interactive specifies that this identity provider is not interactive. func (*identityProvider) Interactive() bool { return false } // Hidden implements idp.IdentityProvider.Hidden. func (*identityProvider) Hidden() bool { return false } // Init initialises the identity provider. func (idp *identityProvider) Init(_ context.Context, params idp.InitParams) error { idp.initParams = params return nil } // URL gets the login URL to use this identity provider. func (idp *identityProvider) URL(dischargeID string) string { return idputil.URL(idp.initParams.URLPrefix, "/login", dischargeID) } func (idp *identityProvider) SetInteraction(ierr *httpbakery.Error, dischargeID string) { ussodischarge.SetInteraction(ierr, idputil.URL(idp.initParams.URLPrefix, "/interact", dischargeID)) } // GetGroups implements idp.IdentityProvider.GetGroups. func (*identityProvider) GetGroups(context.Context, *store.Identity) ([]string, error) { return nil, nil } // Handle handles the Ubuntu SSO Macaroon login process. func (idp *identityProvider) Handle(ctx context.Context, w http.ResponseWriter, req *http.Request) { switch strings.TrimPrefix(req.URL.Path, idp.initParams.URLPrefix) { case "/login": if err := idp.handleLogin(ctx, w, req); err != nil { idp.initParams.VisitCompleter.Failure(ctx, w, req, idputil.DischargeID(req), err) } case "/interact": if err := idp.handleInteract(ctx, w, req); err != nil { idp.initParams.VisitCompleter.Failure(ctx, w, req, idputil.DischargeID(req), err) } default: idp.initParams.VisitCompleter.Failure(ctx, w, req, idputil.DischargeID(req), errgo.WithCausef(nil, params.ErrNotFound, "path %q not found", req.URL.Path)) } } func (idp identityProvider) handleLogin(ctx context.Context, w http.ResponseWriter, req *http.Request) error { switch req.Method { case "GET": m, err := idp.ussoMacaroon(ctx) if err != nil { return err } httprequest.WriteJSON(w, http.StatusOK, ussodischarge.MacaroonResponse{ Macaroon: m, }) case "POST": user, err := idp.verifyUSSOMacaroon(ctx, req) if err != nil { return err } err = idp.initParams.Store.UpdateIdentity( ctx, user, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, }, ) if err != nil { return err } idp.initParams.VisitCompleter.Success(ctx, w, req, idputil.DischargeID(req), user) default: return errgo.WithCausef(nil, params.ErrBadRequest, "unexpected method %q", req.Method) } return nil } func (idp identityProvider) handleInteract(ctx context.Context, w http.ResponseWriter, req *http.Request) error { switch req.Method { case "GET": m, err := idp.ussoMacaroon(ctx) if err != nil { return err } httprequest.WriteJSON(w, http.StatusOK, ussodischarge.MacaroonResponse{ Macaroon: m, }) case "POST": user, err := idp.verifyUSSOMacaroon(ctx, req) if err != nil { return err } err = idp.initParams.Store.UpdateIdentity( ctx, user, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, }, ) if err != nil { return err } token, err := idp.initParams.DischargeTokenCreator.DischargeToken(ctx, user) if err != nil { return err } httprequest.WriteJSON(w, http.StatusOK, ussodischarge.LoginResponse{ DischargeToken: token, }) default: return errgo.WithCausef(nil, params.ErrBadRequest, "unexpected method %q", req.Method) } return nil } func (idp *identityProvider) ussoMacaroon(ctx context.Context) (*bakery.Macaroon, error) { fail := func(err error) (*bakery.Macaroon, error) { return nil, err } // Mint a macaroon that's only good for USSO discharge and can't // used for normal login. m, err := idp.initParams.Oven.NewMacaroon( ctx, bakery.Version1, []checkers.Caveat{checkers.TimeBeforeCaveat(time.Now().Add(ussoMacaroonDuration))}, ussoLoginOp, ) if err != nil { return fail(errgo.Mask(err)) } rootKey, caveatID, err := idp.ussoThirdPartyCaveat() if err != nil { return fail(errgo.Notef(err, "cannot create third-party caveat")) } // We need to add the third party caveat directly to the underlying // macaroon as it's encoded differently from the bakery convention. if err := m.M().AddThirdPartyCaveat(rootKey, caveatID, idp.params.URL); err != nil { return fail(errgo.Notef(err, "cannot create macaroon")) } return m, nil } func (idp identityProvider) ussoThirdPartyCaveat() (rootKey, caveatID []byte, err error) { fail := func(err error) ([]byte, []byte, error) { return nil, nil, err } rootKey = make([]byte, 24) if _, err = rand.Read(rootKey); err != nil { return fail(errgo.Mask(err)) } encryptedKey, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, &idp.params.PublicKey.PublicKey, rootKey[:], nil) if err != nil { return fail(errgo.Mask(err)) } cid := ussoCaveatID{ Secret: base64.StdEncoding.EncodeToString(encryptedKey), Version: 1, } caveatID, err = json.Marshal(cid) if err != nil { return fail(errgo.Mask(err)) } return rootKey, caveatID, nil } // ussoCaveatID is a third-party caveat ID that is understood by Ubuntu // SSO. type ussoCaveatID struct { Secret string `json:"secret"` Version int `json:"version"` } func (idp *identityProvider) verifyUSSOMacaroon(ctx context.Context, req *http.Request) (*store.Identity, error) { var lr ussodischarge.LoginRequest if err := httprequest.Unmarshal(idputil.RequestParams(ctx, nil, req), &lr); err != nil { return nil, errgo.Mask(err) } ussoChecker := ussoCaveatChecker{ namespace: idp.hostname, fallback: httpbakery.NewChecker(), } checker := identchecker.NewChecker(identchecker.CheckerParams{ Checker: &ussoChecker, MacaroonVerifier: idp.initParams.Oven, }) _, err := checker.Auth(lr.Login.Macaroons).Allow(ctx, ussoLoginOp) if err != nil { return nil, errgo.Mask(err, errgo.Is(params.ErrBadRequest)) } if ussoChecker.accountInfo == "" { return nil, errgo.WithCausef(nil, params.ErrBadRequest, "account information not specified") } var acct accountInfo buf, err := base64.StdEncoding.DecodeString(ussoChecker.accountInfo) if err != nil { return nil, ussoCaveatErrorf("account caveat badly formed: %v", err) } if err := json.Unmarshal(buf, &acct); err != nil { return nil, ussoCaveatErrorf("account caveat badly formed: %v", err) } if acct.OpenID == "" { return nil, errgo.WithCausef(nil, params.ErrBadRequest, "account information not specified") } return &store.Identity{ ProviderID: store.MakeProviderIdentity("usso_macaroon", acct.OpenID), Username: acct.OpenID + "@" + idp.params.Domain, Name: acct.DisplayName, Email: acct.Email, }, nil } type ussoCaveatChecker struct { namespace string fallback bakery.FirstPartyCaveatChecker accountInfo string } func (c *ussoCaveatChecker) Namespace() *checkers.Namespace { return nil } // CheckFirstPartyCaveat checks the first party caveats that are added by the // USSO discharger. func (c *ussoCaveatChecker) CheckFirstPartyCaveat(ctx context.Context, caveat string) error { i1 := strings.Index(caveat, "|") if i1 == -1 || caveat[0:i1] != c.namespace { return c.fallback.CheckFirstPartyCaveat(ctx, caveat) } i2 := strings.Index(caveat[i1+1:], "|") if i2 == -1 { return errgo.WithCausef(nil, checkers.ErrCaveatNotRecognized, "verification failed (USSO caveat): no argument provided in %q", caveat) } i2 += i1 + 1 cond, arg := caveat[i1+1:i2], caveat[i2+1:] switch cond { case "account": if c.accountInfo != "" && c.accountInfo != arg { return ussoCaveatErrorf("account specified inconsistently") } c.accountInfo = arg return nil case "valid_since": // We don't check the valid_since value to prevent // problems with slight clock skew between services. return nil case "last_auth": // TODO(mhilton) work out if there is anything we should // check with this. return nil case "expires": t, err := time.Parse(timeFormat, arg) if err != nil { return ussoCaveatErrorf("expires caveat badly formed: %v", err) } if time.Now().After(t) { return ussoCaveatErrorf("expires before current time") } return nil default: return errgo.WithCausef(nil, checkers.ErrCaveatNotRecognized, "verification failed (USSO caveat): unknown caveat %q", cond) } } func ussoCaveatErrorf(f string, a ...interface{}) error { return errgo.Newf("verification failed (USSO caveat): %s", fmt.Sprintf(f, a...)) } type accountInfo struct { OpenID string `json:"openid"` Email string `json:"email"` DisplayName string `json:"displayname"` Username string `json:"username"` IsVerified bool `json:"is_verified"` } golang-github-canonical-candid-1.12.3/idp/usso/ussodischarge/ussodischarge_test.go000066400000000000000000000422471457263123000303470ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package ussodischarge_test import ( "bytes" "crypto/rsa" "crypto/sha1" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "net/http" "net/http/httptest" "testing" "time" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" macaroon "gopkg.in/macaroon.v2" yaml "gopkg.in/yaml.v2" udclient "github.com/canonical/candid/candidclient/ussodischarge" "github.com/canonical/candid/config" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idptest" "github.com/canonical/candid/idp/usso/ussodischarge" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/store" ) func TestConfig(t *testing.T) { c := qt.New(t) configYaml := ` identity-providers: - type: usso_macaroon url: https://login.ubuntu.com domain: ussotest public-key: | -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAviBpgY6CXh9MEZTBJUbV v34mHCHLGCQnfP2OMQjDXkqvdXZp6EEO9wgvO6Xjh1ByoP2K0Qqbikfgi/I5cOrn JKPrt85RrKfWYIwTykBUaPWO5AsAxvb/+L4gXXDNYaYxL/kmRpP+45qVmOFdK4yX adtdzYUBmH8BFfTPn5RwlHU+9jrFJkidlWDCrocJiJZdeBYxlu4kIK1hLF1HhnQ9 sG2iy/4GUS9QFuQVwBHPett2ANC/lUn0MbPNo8YVOTg9pswKteFNWUP7pcGWwPIn ktjFFKBWXIaTZs33KeWfFzUkArwCqmz7YOCms+pB3V6YrTCO0gOqXRnj6SqUkKWA xQIDAQAB -----END PUBLIC KEY----- ` var conf config.Config err := yaml.Unmarshal([]byte(configYaml), &conf) c.Assert(err, qt.IsNil) c.Assert(conf.IdentityProviders, qt.HasLen, 1) c.Assert(conf.IdentityProviders[0].Name(), qt.Equals, "usso_macaroon") } func TestSuite(t *testing.T) { qtsuite.Run(qt.New(t), &ussoMacaroonSuite{}) } type ussoMacaroonSuite struct { *fixture } type fixture struct { idptest *idptest.Fixture idp idp.IdentityProvider } func newFixture(c *qt.C) *fixture { f := &fixture{ idptest: idptest.NewFixture(c, candidtest.NewStore()), } var err error f.idp, err = ussodischarge.NewIdentityProvider(ussodischarge.Params{ URL: "https://login.staging.ubuntu.com", Domain: "ussotest", PublicKey: ussodischarge.PublicKey{ PublicKey: testKey.PublicKey, }, }) c.Assert(err, qt.IsNil) err = f.idp.Init(f.idptest.Ctx, f.idptest.InitParams(c, "https://idp.test")) c.Assert(err, qt.IsNil) return f } func (s *ussoMacaroonSuite) Init(c *qt.C) { s.fixture = newFixture(c) } func (s *ussoMacaroonSuite) TestName(c *qt.C) { c.Assert(s.idp.Name(), qt.Equals, "usso_macaroon") } func (s *ussoMacaroonSuite) TestDescription(c *qt.C) { c.Assert(s.idp.Description(), qt.Equals, "Ubuntu SSO macaroon discharge authentication") } func (s *ussoMacaroonSuite) TestIconURL(c *qt.C) { c.Assert(s.idp.IconURL(), qt.Equals, "") } func (s *ussoMacaroonSuite) TestInteractive(c *qt.C) { c.Assert(s.idp.Interactive(), qt.Equals, false) } func (s *ussoMacaroonSuite) TestHidden(c *qt.C) { c.Assert(s.idp.Hidden(), qt.Equals, false) } func (s *ussoMacaroonSuite) TestURL(c *qt.C) { t := s.idp.URL("1") c.Assert(t, qt.Equals, "https://idp.test/login?id=1") } func (s *ussoMacaroonSuite) TestHandleGetSuccess(c *qt.C) { req, err := http.NewRequest("GET", "/login", nil) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() s.idp.Handle(s.idptest.Ctx, rr, req) c.Assert(rr.Code, qt.Equals, http.StatusOK, qt.Commentf("%s", rr.Body)) var mresp udclient.MacaroonResponse err = json.Unmarshal(rr.Body.Bytes(), &mresp) c.Assert(err, qt.IsNil) m := mresp.Macaroon.M() cavs := m.Caveats() var thirdPartyCav macaroon.Caveat for _, cav := range cavs { if len(cav.VerificationId) != 0 { thirdPartyCav = cav } } c.Assert(thirdPartyCav.VerificationId, qt.Not(qt.HasLen), 0) var cid ussodischarge.USSOCaveatID err = json.Unmarshal(thirdPartyCav.Id, &cid) c.Assert(err, qt.IsNil) c.Assert(cid.Version, qt.Equals, 1) secret, err := base64.StdEncoding.DecodeString(cid.Secret) c.Assert(err, qt.IsNil) rk, err := rsa.DecryptOAEP(sha1.New(), nil, testKey, secret, nil) c.Assert(err, qt.IsNil) md, err := macaroon.New(rk, thirdPartyCav.Id, "test", macaroon.V1) c.Assert(err, qt.IsNil) md.Bind(m.Signature()) ms := macaroon.Slice{m, md} checker := identchecker.NewChecker(identchecker.CheckerParams{ MacaroonVerifier: s.idptest.Oven, }) authInfo, err := checker.Auth(ms).Allow(s.idptest.Ctx, ussodischarge.USSOLoginOp) c.Assert(err, qt.IsNil) c.Assert(authInfo.Identity, qt.Equals, nil) } func (s *ussoMacaroonSuite) TestHandleGetV1Success(c *qt.C) { req, err := http.NewRequest("GET", "/interact", nil) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() s.idp.Handle(s.idptest.Ctx, rr, req) c.Assert(rr.Code, qt.Equals, http.StatusOK, qt.Commentf("%s", rr.Body)) var mresp udclient.MacaroonResponse err = json.Unmarshal(rr.Body.Bytes(), &mresp) c.Assert(err, qt.IsNil) m := mresp.Macaroon.M() cavs := m.Caveats() var thirdPartyCav macaroon.Caveat for _, cav := range cavs { if len(cav.VerificationId) != 0 { thirdPartyCav = cav } } c.Assert(thirdPartyCav.VerificationId, qt.Not(qt.HasLen), 0) var cid ussodischarge.USSOCaveatID err = json.Unmarshal(thirdPartyCav.Id, &cid) c.Assert(err, qt.IsNil) c.Assert(cid.Version, qt.Equals, 1) secret, err := base64.StdEncoding.DecodeString(cid.Secret) c.Assert(err, qt.IsNil) rk, err := rsa.DecryptOAEP(sha1.New(), nil, testKey, secret, nil) c.Assert(err, qt.IsNil) md, err := macaroon.New(rk, thirdPartyCav.Id, "test", macaroon.V1) c.Assert(err, qt.IsNil) md.Bind(m.Signature()) ms := macaroon.Slice{m, md} checker := identchecker.NewChecker(identchecker.CheckerParams{ MacaroonVerifier: s.idptest.Oven, }) authInfo, err := checker.Auth(ms).Allow(s.idptest.Ctx, ussodischarge.USSOLoginOp) c.Assert(err, qt.IsNil) c.Assert(authInfo.Identity, qt.Equals, nil) } var postTests = []struct { about string account *ussodischarge.AccountInfo validSince string lastAuth string expires string extraCaveats []string expectUser *store.Identity expectError string }{{ about: "success", account: &ussodischarge.AccountInfo{ Username: "username", OpenID: "1234567", Email: "testuser@example.com", DisplayName: "Test User", }, validSince: timeString(-time.Hour), lastAuth: timeString(-time.Minute), expires: timeString(time.Hour), expectUser: &store.Identity{ ProviderID: store.MakeProviderIdentity("usso_macaroon", "1234567"), Username: "1234567@ussotest", Name: "Test User", Email: "testuser@example.com", }, }, { about: "no account", validSince: timeString(-time.Hour), lastAuth: timeString(-time.Minute), expires: timeString(time.Hour), expectError: "account information not specified", }, { about: "expires bad format", account: &ussodischarge.AccountInfo{ Username: "username", OpenID: "1234567", Email: "testuser@example.com", DisplayName: "Test User", }, validSince: timeString(-time.Hour), lastAuth: timeString(-time.Minute), expires: "never", expectError: `verification failed \(USSO caveat\): expires caveat badly formed: parsing time "never" as "2006-01-02T15:04:05.000000": cannot parse "never" as "2006"`, }, { about: "expires in past", account: &ussodischarge.AccountInfo{ Username: "username", OpenID: "1234567", Email: "testuser@example.com", DisplayName: "Test User", }, validSince: timeString(-time.Hour), lastAuth: timeString(-time.Minute), expires: timeString(-time.Hour), expectError: `verification failed \(USSO caveat\): expires before current time`, }, { about: "multiple account info", account: &ussodischarge.AccountInfo{ Username: "username", OpenID: "1234567", Email: "testuser@example.com", DisplayName: "Test User", }, validSince: timeString(-time.Hour), lastAuth: timeString(-time.Minute), expires: timeString(time.Hour), extraCaveats: []string{`login.staging.ubuntu.com|account|{"username": "failuser"}`}, expectError: `verification failed \(USSO caveat\): account specified inconsistently`, }, { about: "unrecognised caveat", account: &ussodischarge.AccountInfo{ Username: "username", OpenID: "1234567", Email: "testuser@example.com", DisplayName: "Test User", }, validSince: timeString(-time.Hour), lastAuth: timeString(-time.Minute), expires: timeString(time.Hour), extraCaveats: []string{`login.staging.ubuntu.com|no-such-condition|fail`}, expectError: `verification failed \(USSO caveat\): unknown caveat "no-such-condition"`, }, { about: "account bad base64", validSince: timeString(-time.Hour), lastAuth: timeString(-time.Minute), expires: timeString(time.Hour), extraCaveats: []string{`login.staging.ubuntu.com|account|f`}, expectError: `verification failed \(USSO caveat\): account caveat badly formed: illegal base64 data at input byte 0`, }, { about: "account bad json", validSince: timeString(-time.Hour), lastAuth: timeString(-time.Minute), expires: timeString(time.Hour), extraCaveats: []string{`login.staging.ubuntu.com|account|fQ==`}, expectError: `verification failed \(USSO caveat\): account caveat badly formed: invalid character '}' looking for beginning of value`, }, { about: "without argument", account: &ussodischarge.AccountInfo{ Username: "username", OpenID: "1234567", Email: "testuser@example.com", DisplayName: "Test User", }, validSince: timeString(-time.Hour), lastAuth: timeString(-time.Minute), expires: timeString(time.Hour), extraCaveats: []string{`login.staging.ubuntu.com|no-arg`}, expectError: `verification failed \(USSO caveat\): no argument provided in "login.staging.ubuntu.com\|no-arg"`, }} func (s *ussoMacaroonSuite) TestHandlePostV1(c *qt.C) { err := s.idp.Init(s.idptest.Ctx, s.idptest.InitParams(c, "https://idp.test")) c.Assert(err, qt.IsNil) bm, err := s.idptest.Oven.NewMacaroon(s.idptest.Ctx, bakery.Version1, nil, ussodischarge.USSOLoginOp) c.Assert(err, qt.IsNil) m := bm.M() buf, err := json.Marshal(&ussodischarge.AccountInfo{ Username: "username", OpenID: "1234567", Email: "testuser@example.com", DisplayName: "Test User", }) c.Assert(err, qt.IsNil) err = m.AddFirstPartyCaveat([]byte("login.staging.ubuntu.com|account|" + base64.StdEncoding.EncodeToString(buf))) c.Assert(err, qt.IsNil) err = m.AddFirstPartyCaveat([]byte("login.staging.ubuntu.com|valid_since|" + timeString(-time.Hour))) c.Assert(err, qt.IsNil) err = m.AddFirstPartyCaveat([]byte("login.staging.ubuntu.com|last_auth|" + timeString(-time.Minute))) c.Assert(err, qt.IsNil) err = m.AddFirstPartyCaveat([]byte("login.staging.ubuntu.com|expires|" + timeString(time.Hour))) c.Assert(err, qt.IsNil) body := udclient.Login{ Macaroons: macaroon.Slice{m}, } buf, err = json.Marshal(body) c.Assert(err, qt.IsNil) req, err := http.NewRequest("POST", "/interact", bytes.NewReader(buf)) c.Assert(err, qt.IsNil) req.Header.Set("Content-Type", "application/json") req.ParseForm() rr := httptest.NewRecorder() s.idp.Handle(s.idptest.Ctx, rr, req) c.Assert(rr.Code, qt.Equals, http.StatusOK) c.Assert(rr.HeaderMap.Get("Content-Type"), qt.Equals, "application/json") var resp udclient.LoginResponse err = json.Unmarshal(rr.Body.Bytes(), &resp) c.Assert(err, qt.IsNil) c.Assert(resp, qt.DeepEquals, udclient.LoginResponse{ DischargeToken: &httpbakery.DischargeToken{ Kind: "test", Value: []byte("1234567@ussotest"), }, }) } func TestHandlePost(t *testing.T) { c := qt.New(t) defer c.Done() for _, test := range postTests { c.Run(test.about, func(c *qt.C) { f := newFixture(c) err := f.idp.Init(f.idptest.Ctx, f.idptest.InitParams(c, "https://idp.test")) c.Assert(err, qt.IsNil) bm, err := f.idptest.Oven.NewMacaroon(f.idptest.Ctx, bakery.Version1, nil, ussodischarge.USSOLoginOp) c.Assert(err, qt.IsNil) m := bm.M() if test.account != nil { buf, err := json.Marshal(test.account) c.Assert(err, qt.IsNil) err = m.AddFirstPartyCaveat([]byte("login.staging.ubuntu.com|account|" + base64.StdEncoding.EncodeToString(buf))) c.Assert(err, qt.IsNil) } if test.validSince != "" { err = m.AddFirstPartyCaveat([]byte("login.staging.ubuntu.com|valid_since|" + test.validSince)) c.Assert(err, qt.IsNil) } if test.lastAuth != "" { err = m.AddFirstPartyCaveat([]byte("login.staging.ubuntu.com|last_auth|" + test.lastAuth)) c.Assert(err, qt.IsNil) } if test.expires != "" { err = m.AddFirstPartyCaveat([]byte("login.staging.ubuntu.com|expires|" + test.expires)) c.Assert(err, qt.IsNil) } for _, cav := range test.extraCaveats { err = m.AddFirstPartyCaveat([]byte(cav)) c.Assert(err, qt.IsNil) } body := udclient.Login{ Macaroons: macaroon.Slice{m}, } buf, err := json.Marshal(body) c.Assert(err, qt.IsNil) req, err := http.NewRequest("POST", "/login", bytes.NewReader(buf)) c.Assert(err, qt.IsNil) req.Header.Set("Content-Type", "application/json") req.ParseForm() rr := httptest.NewRecorder() f.idp.Handle(f.idptest.Ctx, rr, req) if test.expectError != "" { f.idptest.AssertLoginFailureMatches(c, test.expectError) return } f.idptest.AssertLoginSuccess(c, test.expectUser.Username) f.idptest.Store.AssertUser(c, test.expectUser) }) } } func TestMultipleLogins(t *testing.T) { c := qt.New(t) defer c.Done() f := newFixture(c) err := f.idp.Init(f.idptest.Ctx, f.idptest.InitParams(c, "https://idp.test")) c.Assert(err, qt.IsNil) dologin := func(ai *ussodischarge.AccountInfo) { bm, err := f.idptest.Oven.NewMacaroon(f.idptest.Ctx, bakery.Version1, nil, ussodischarge.USSOLoginOp) c.Assert(err, qt.IsNil) m := bm.M() buf, err := json.Marshal(ai) c.Assert(err, qt.IsNil) err = m.AddFirstPartyCaveat([]byte("login.staging.ubuntu.com|account|" + base64.StdEncoding.EncodeToString(buf))) c.Assert(err, qt.IsNil) err = m.AddFirstPartyCaveat([]byte("login.staging.ubuntu.com|valid_since|" + timeString(-time.Minute))) c.Assert(err, qt.IsNil) err = m.AddFirstPartyCaveat([]byte("login.staging.ubuntu.com|last_auth|" + timeString(-time.Hour))) c.Assert(err, qt.IsNil) err = m.AddFirstPartyCaveat([]byte("login.staging.ubuntu.com|expires|" + timeString(time.Hour))) c.Assert(err, qt.IsNil) body := udclient.Login{ Macaroons: macaroon.Slice{m}, } buf, err = json.Marshal(body) c.Assert(err, qt.IsNil) req, err := http.NewRequest("POST", "/login", bytes.NewReader(buf)) c.Assert(err, qt.IsNil) req.Header.Set("Content-Type", "application/json") req.ParseForm() rr := httptest.NewRecorder() f.idp.Handle(f.idptest.Ctx, rr, req) f.idptest.AssertLoginSuccess(c, ai.OpenID+"@ussotest") f.idptest.Store.AssertUser(c, &store.Identity{ ProviderID: store.MakeProviderIdentity("usso_macaroon", ai.OpenID), Username: ai.OpenID + "@ussotest", Name: ai.DisplayName, Email: ai.Email, }) } dologin(&ussodischarge.AccountInfo{ Username: "username1", OpenID: "1234568", Email: "testuser1@example.com", DisplayName: "Test User", }) f.idptest.Reset() dologin(&ussodischarge.AccountInfo{ Username: "username2", OpenID: "1234569", Email: "testuser2@example.com", DisplayName: "Test User II", }) } func timeString(d time.Duration) string { return time.Now().Add(d).UTC().Format(ussodischarge.TimeFormat) } var testKey *rsa.PrivateKey func init() { block, _ := pem.Decode([]byte(testKeyPEM)) var err error testKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { panic(err) } } // testKey generated by openssl const testKeyPEM = ` -----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEA2gZGJUvSl0NHkq5u4RDwNYIe3Yc5U5a95anmFVTZU70HNICJ GAZkEgDZPnbz/gv41r4qMXO30I27jHNUJluq7KeEnG0SJPpiZcawZ4GTvao5e9aQ cD0alENo0qyyND1xseyQ1VEKlb1D6Sik9y+yyNcgbE8H5MjO/zERL6IsAPILeYJv oAYnLmv0fj49HIR8c+U7Xz61M0Khwt2U/3Ou4JnRH+6gdhLRvsUbd9GFu6GsEHQn YAH9xrkY8458Oj6L7kEbezVbVH6J4a3LtLo0MZrDNeCGKRL0rNf6me3FRctSFHhJ bCUlnGOF/FkVnVleyFAzebTA3Wvnrnax4XtlyQIDAQABAoIBAQDVe8c7xd7TVoHC 0yKnJxrOijcG2936R2RyecZdpNOY90MS2blj2P4r0sDmNTv8ymRCgbp26cRXZjD6 +gKv/JqFWBK1yOc3ZiTrW35oG606zm+zHwoXnP1lqAwAHjHwjSnC+s1m0w/2R3kz 2SSPFhmOJ3gMFea40xg9MSKO7dEAqjGnnj0iRkGqLw1KJArvcj6qaBAq3xFBC9+A /YWCj6xkyFRzbQ0pu08TwtU6m4WVkAzXAUWo8wdrhAAn569AQiIQB0OMEqhNWGUw ZMimterRyfqIbDWGaEeQkocveRikogf7DnC0gOy8DSacGDzdFeI8GBuNURdTeFDS Di3mc++xAoGBAPrzhsoso3MCgBJF1v21+DBrR62BORJoXDXvQtTW5T5Vrc18KTEg sBBUgMlqaW21TYiCDm9wFqm7B1h6gVMupebvp0wFXogearLH1/uMsILxjhLFRfZS S+6GSB0i/w7k7zVpQ6UNz3x6Ab/DsM1Fqi8NSGxSl+undsxmyX3IxMj7AoGBAN5p KjTGhknU43HIH6z66Z8q6ZBH8spBCsTxkV+yhFO00oqvjrtHPvHNAHudaqVuroKq Fyiyf2B/f1g5s8mzpq1xdHDF/7i9BiemN9FKlznUDHLV9+c5xKeNrzjHd4c78Lnj z9NyOgSc0DgFwj2xn6WdJjm8Mogu/FFpu6kr3tkLAoGBANBeBD06czy7hrulYa2n ujv517oo4cp2/JmL4GH5TL9FRNqpjUpNaeMlRwn2YTPGpmoCExpUZ3zm3mKI1XjL 8tSdiLuGecdr+gwYAy3K04TmLKFJS54LFyEmPhpzRHSJglVG4fPaU713UJx5UAQh I/2NeeT3b00r72gosITQfxShAoGBAKdkhVSVOkrlRI3Fbjm12xFlrcZesFgTHfTe T2i0Ji4OAQxKV2WSmMhKX5up/bMnG4bSV33U4lORghm3zB357W/K3TVngDDda315 97a4qhrnAruHWP6Zlu34kDFuxwJsVaDC2g8tgIcqMviHNQtT3XE7VqLLh0jB/DuW FZycnSvDAoGAM7vHz15cvi1ivq4I3KNmHTmQa2oMZ5NeBXRGFC5ptnPG4jkNaOIa 1vFY042om4AF5dt5OJaO7wmYwrWOUpVnFEvOS9gi/ucLahgplGJCg3tY3j52J6Gp OJYm1M7VAWvhigBzfa2tw7w76HbyF79t4e67tVJACs1ABk4Sqr1Ds9Q= -----END RSA PRIVATE KEY----- ` golang-github-canonical-candid-1.12.3/idp/usso/ussooauth/000077500000000000000000000000001457263123000232745ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/idp/usso/ussooauth/discharge_test.go000066400000000000000000000044331457263123000266170ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package ussooauth_test import ( "context" "testing" qt "github.com/frankban/quicktest" "github.com/juju/usso" "github.com/canonical/candid/candidclient/ussologin" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/usso/internal/mockusso" "github.com/canonical/candid/idp/usso/ussooauth" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/internal/discharger" "github.com/canonical/candid/internal/identity" "github.com/canonical/candid/store" ) func TestDischarge(t *testing.T) { c := qt.New(t) defer c.Done() testStore := candidtest.NewStore() sp := testStore.ServerParams() sp.IdentityProviders = []idp.IdentityProvider{ ussooauth.IdentityProvider, } candid := candidtest.NewServer(c, sp, map[string]identity.NewAPIHandlerFunc{ "discharger": discharger.NewAPIHandler, }) dischargeCreator := candidtest.NewDischargeCreator(candid) ussoSrv := mockusso.NewServer() defer ussoSrv.Close() err := testStore.Store.UpdateIdentity( candid.Ctx, &store.Identity{ ProviderID: store.MakeProviderIdentity("usso", "https://login.ubuntu.com/+id/1234"), Username: "test", Name: "Test User", Email: "test@example.com", Groups: []string{"test"}, }, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, store.Groups: store.Set, }, ) c.Assert(err, qt.IsNil) ussoSrv.MockUSSO.AddUser(&mockusso.User{ ID: "1234", NickName: "test", FullName: "Test User", Email: "test@example.com", Groups: []string{ "test", }, ConsumerSecret: "secret1", TokenKey: "test-token", TokenSecret: "secret2", }) ussoSrv.MockUSSO.SetLoginUser("1234") interactor := ussologin.NewInteractor(tokenGetterFunc(func(_ context.Context) (*usso.SSOData, error) { return &usso.SSOData{ ConsumerKey: "1234", ConsumerSecret: "secret1", TokenKey: "test-token", TokenName: "test-token", TokenSecret: "secret2", }, nil })) dischargeCreator.AssertDischarge(c, interactor) } type tokenGetterFunc func(context.Context) (*usso.SSOData, error) func (f tokenGetterFunc) GetToken(ctx context.Context) (*usso.SSOData, error) { return f(ctx) } golang-github-canonical-candid-1.12.3/idp/usso/ussooauth/ussooauth.go000066400000000000000000000132531457263123000256610ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Pacakge ussooauth is an identity provider that authenticates against // Ubuntu SSO using OAuth. package ussooauth import ( "bytes" "context" "encoding/json" "io/ioutil" "mime" "net/http" "net/url" "regexp" "strings" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/canonical/candid/candidclient/ussologin" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil" "github.com/canonical/candid/store" ) func init() { idp.Register("usso_oauth", func(func(interface{}) error) (idp.IdentityProvider, error) { return IdentityProvider, nil }) } // IdentityProvider is an idp.IdentityProvider that provides // authentication via Ubuntu SSO using OAuth. var IdentityProvider idp.IdentityProvider = &identityProvider{} const ( ussoURL = "https://login.ubuntu.com" ) // identityProvider allows login using request signing with // Ubuntu SSO OAuth tokens. type identityProvider struct { initParams idp.InitParams } // Name gives the name of the identity provider (usso_oauth). func (*identityProvider) Name() string { return "usso_oauth" } // Domain implements idp.IdentityProvider.Domain. func (*identityProvider) Domain() string { return "" } // Description gives a description of the identity provider. func (*identityProvider) Description() string { return "Ubuntu SSO OAuth" } // IconURL returns the URL of an icon for the identity provider. func (*identityProvider) IconURL() string { return "" } // Interactive specifies that this identity provider is not interactive. func (*identityProvider) Interactive() bool { return false } // Hidden implements idp.IdentityProvider.Hidden. func (*identityProvider) Hidden() bool { return false } // Init initialises the identity provider. func (idp *identityProvider) Init(_ context.Context, params idp.InitParams) error { idp.initParams = params return nil } // URL gets the login URL to use this identity provider. func (idp *identityProvider) URL(dischargeID string) string { return idputil.URL(idp.initParams.URLPrefix, "/login", dischargeID) } // SetInteraction sets the interaction information for func (idp *identityProvider) SetInteraction(ierr *httpbakery.Error, dischargeID string) { ussologin.SetInteraction(ierr, idputil.URL(idp.initParams.URLPrefix, "/interact", dischargeID)) } // GetGroups implements idp.IdentityProvider.GetGroups. func (*identityProvider) GetGroups(context.Context, *store.Identity) ([]string, error) { // This method should never be called as this IDP supports // identities in the "usso" namespace. return nil, nil } // Handle handles the Ubuntu SSO OAuth login process. func (idp *identityProvider) Handle(ctx context.Context, w http.ResponseWriter, req *http.Request) { id, err := verifyOAuthSignature(idp.initParams.URLPrefix+req.URL.Path, req) if err != nil { idp.initParams.VisitCompleter.Failure(ctx, w, req, idputil.DischargeID(req), err) return } identity := store.Identity{ ProviderID: store.MakeProviderIdentity("usso", id), } if err := idp.initParams.Store.Identity(ctx, &identity); err != nil { idp.initParams.VisitCompleter.Failure(ctx, w, req, idputil.DischargeID(req), errgo.Notef(err, "cannot get user details for %q", id)) return } if strings.TrimPrefix(req.URL.Path, idp.initParams.URLPrefix) == "/interact" { token, err := idp.initParams.DischargeTokenCreator.DischargeToken(ctx, &identity) if err != nil { code, body := httpbakery.ErrorToResponse(ctx, err) httprequest.WriteJSON(w, code, body) return } httprequest.WriteJSON(w, http.StatusOK, ussologin.LoginResponse{ DischargeToken: token, }) } else { idp.initParams.VisitCompleter.Success(ctx, w, req, idputil.DischargeID(req), &identity) } } var consumerKeyRegexp = regexp.MustCompile(`oauth_consumer_key="([^"]*)"`) // verifyOAuthSignature verifies with Ubuntu SSO that the request is correctly // signed. func verifyOAuthSignature(requestURL string, req *http.Request) (string, error) { req.ParseForm() u, err := url.Parse(requestURL) if err != nil { return "", errgo.Notef(err, "cannot parse request URL") } u.RawQuery = "" request := struct { URL string `json:"http_url"` Method string `json:"http_method"` Authorization string `json:"authorization"` QueryString string `json:"query_string"` }{ URL: u.String(), Method: req.Method, Authorization: req.Header.Get("Authorization"), QueryString: req.Form.Encode(), } buf, err := json.Marshal(request) if err != nil { return "", errgo.Notef(err, "cannot marshal request") } resp, err := http.Post(ussoURL+"/api/v2/requests/validate", "application/json", bytes.NewReader(buf)) if err != nil { return "", errgo.Mask(err) } defer resp.Body.Close() t, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) if err != nil { return "", errgo.Newf("bad content type %q", resp.Header.Get("Content-Type")) } if t != "application/json" { return "", errgo.Newf("unexpected response type %q", t) } var validated struct { IsValid bool `json:"is_valid"` Error string `json:"error"` } data, err := ioutil.ReadAll(resp.Body) if err := json.Unmarshal(data, &validated); err != nil { return "", errgo.Mask(err) } if validated.Error != "" { return "", errgo.Newf("cannot validate OAuth credentials: %s", validated.Error) } if !validated.IsValid { return "", errgo.Newf("invalid OAuth credentials") } consumerKey := consumerKeyRegexp.FindStringSubmatch(req.Header.Get("Authorization")) if len(consumerKey) != 2 { return "", errgo.Newf("no customer key in authorization") } return ussoURL + "/+id/" + consumerKey[1], nil } golang-github-canonical-candid-1.12.3/idp/usso/ussooauth/ussooauth_test.go000066400000000000000000000110061457263123000267120ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package ussooauth_test import ( "net/http" "net/http/httptest" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/gomodule/oauth1/oauth" "gopkg.in/yaml.v2" "github.com/canonical/candid/config" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idptest" "github.com/canonical/candid/idp/usso/internal/mockusso" "github.com/canonical/candid/idp/usso/ussooauth" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/store" ) func TestConfig(t *testing.T) { c := qt.New(t) configYaml := ` identity-providers: - type: usso_oauth ` var conf config.Config err := yaml.Unmarshal([]byte(configYaml), &conf) c.Assert(err, qt.IsNil) c.Assert(conf.IdentityProviders, qt.HasLen, 1) c.Assert(conf.IdentityProviders[0].Name(), qt.Equals, "usso_oauth") } func TestUSSOAuth(t *testing.T) { qtsuite.Run(qt.New(t), &ussooauthSuite{}) } type ussooauthSuite struct { idptest *idptest.Fixture idp idp.IdentityProvider } func (s *ussooauthSuite) Init(c *qt.C) { s.idptest = idptest.NewFixture(c, candidtest.NewStore()) s.idp = ussooauth.IdentityProvider err := s.idp.Init(s.idptest.Ctx, s.idptest.InitParams(c, "https://idp.test")) c.Assert(err, qt.IsNil) } func (s *ussooauthSuite) TestName(c *qt.C) { c.Assert(s.idp.Name(), qt.Equals, "usso_oauth") } func (s *ussooauthSuite) TestDescription(c *qt.C) { c.Assert(s.idp.Description(), qt.Equals, "Ubuntu SSO OAuth") } func (s *ussooauthSuite) TestIconURL(c *qt.C) { c.Assert(s.idp.IconURL(), qt.Equals, "") } func (s *ussooauthSuite) TestInteractive(c *qt.C) { c.Assert(s.idp.Interactive(), qt.Equals, false) } func (s *ussooauthSuite) TestHidden(c *qt.C) { c.Assert(s.idp.Hidden(), qt.Equals, false) } func (s *ussooauthSuite) TestURL(c *qt.C) { t := s.idp.URL("1") c.Assert(t, qt.Equals, "https://idp.test/login?id=1") } func (s *ussooauthSuite) TestHandleSuccess(c *qt.C) { ussoSrv := mockusso.NewServer() defer ussoSrv.Close() err := s.idptest.Store.Store.UpdateIdentity( s.idptest.Ctx, &store.Identity{ ProviderID: store.MakeProviderIdentity("usso", "https://login.ubuntu.com/+id/test"), Username: "test", Name: "Test User", Email: "test@example.com", }, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, }, ) c.Assert(err, qt.IsNil) ussoSrv.MockUSSO.AddUser(&mockusso.User{ ID: "test", NickName: "test", FullName: "Test User", Email: "test@example.com", ConsumerSecret: "secret1", TokenKey: "test-token", TokenSecret: "secret2", }) oc := &oauth.Client{ Credentials: oauth.Credentials{ Token: "test", Secret: "secret1", }, SignatureMethod: oauth.HMACSHA1, } req, err := http.NewRequest("GET", "http://example.com/oauth?id=2", nil) c.Assert(err, qt.IsNil) err = oc.SetAuthorizationHeader( req.Header, &oauth.Credentials{ Token: "test-token", Secret: "secret2", }, req.Method, req.URL, nil, ) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() s.idp.Handle(s.idptest.Ctx, rr, req) s.idptest.AssertLoginSuccess(c, "test") } func (s *ussooauthSuite) TestHandleVerifyFail(c *qt.C) { ussoSrv := mockusso.NewServer() defer ussoSrv.Close() err := s.idptest.Store.Store.UpdateIdentity( s.idptest.Ctx, &store.Identity{ ProviderID: store.MakeProviderIdentity("usso", "https://login.ubuntu.com/+id/test"), Username: "test", Name: "Test User", Email: "test@example.com", }, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, }, ) c.Assert(err, qt.IsNil) ussoSrv.MockUSSO.AddUser(&mockusso.User{ ID: "test", NickName: "test", FullName: "Test User", Email: "test@example.com", ConsumerSecret: "secret1", TokenKey: "test-token", TokenSecret: "secret2", }) oc := &oauth.Client{ Credentials: oauth.Credentials{ Token: "test", Secret: "secret1", }, SignatureMethod: oauth.HMACSHA1, } req, err := http.NewRequest("GET", "http://example.com/oauth?id=2", nil) c.Assert(err, qt.IsNil) err = oc.SetAuthorizationHeader( req.Header, &oauth.Credentials{ Token: "test-token2", Secret: "secret2", }, req.Method, req.URL, nil, ) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() s.idp.Handle(s.idptest.Ctx, rr, req) s.idptest.AssertLoginFailureMatches(c, `invalid OAuth credentials`) } golang-github-canonical-candid-1.12.3/internal/000077500000000000000000000000001457263123000213115ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/internal/auth/000077500000000000000000000000001457263123000222525ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/internal/auth/auth.go000066400000000000000000000456711457263123000235570ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package auth import ( "context" "sort" "strings" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/juju/aclstore/v2" "github.com/juju/loggo" "gopkg.in/errgo.v1" macaroon "gopkg.in/macaroon.v2" "github.com/canonical/candid/idp" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) var logger = loggo.GetLogger("candid.internal.auth") const ( AdminUsername = "admin@candid" SSHKeyGetterGroup = "sshkeygetter@candid" GroupListGroup = "grouplist@candid" UserInformationGroup = "userinfo@candid" ) var AdminProviderID = store.MakeProviderIdentity("idm", "admin") const ( kindGlobal = "global" kindUser = "u" kindUserID = "uid" ) // The following constants define possible operation actions. const ( ActionRead = "read" ActionVerify = "verify" ActionDischargeFor = "dischargeFor" ActionDischarge = "discharge" ActionCreateAgent = "createAgent" ActionCreateParentAgent = "createParentAgent" ActionReadAdmin = "readAdmin" ActionWriteAdmin = "writeAdmin" ActionReadGroups = "readGroups" ActionWriteGroups = "writeGroups" ActionReadSSHKeys = "readSSHKeys" ActionWriteSSHKeys = "writeSSHKeys" ActionLogin = "login" ActionReadDischargeToken = "read-discharge-token" ActionClearUserMFACredentials = "clearUserMFACredentials" ) const ( dischargeForUserACL = "discharge-for-user" readUserACL = "read-user" readUserGroupsACL = "read-user-groups" readUserSSHKeysACL = "read-user-ssh-keys" writeUserACL = "write-user" writeUserSSHKeysACL = "write-user-ssh-keys" clearUserMFACredentialsACL = "clear-user-mfa-credentials" ) var aclDefaults = map[string][]string{ dischargeForUserACL: {AdminUsername}, readUserACL: {AdminUsername, UserInformationGroup}, readUserGroupsACL: {AdminUsername, GroupListGroup, UserInformationGroup}, readUserSSHKeysACL: {AdminUsername, SSHKeyGetterGroup, UserInformationGroup}, writeUserACL: {AdminUsername}, writeUserSSHKeysACL: {AdminUsername}, clearUserMFACredentialsACL: {AdminUsername}, } // An Authorizer is used to authorize operations in the identity server. type Authorizer struct { adminPassword string location string checker *identchecker.Checker store store.Store groupResolvers map[string]groupResolver aclManager *aclstore.Manager } // Params specifify the configuration parameters for a new Authroizer. type Params struct { // AdminPassword is the password of the admin user in the // identity server. AdminPassword string // Location is the url of the discharger that third-party caveats // will be addressed to. This should be the address of this // identity server. Location string // MacaroonVerifier is the store of macaroon operations and root // keys. MacaroonVerifier bakery.MacaroonVerifier // Store is the identity store. Store store.Store // IdentityProviders contains the set of identity providers that // are configured for the service. The authenticatore uses these // to get group information for authenticated users. IdentityProviders []idp.IdentityProvider // ACLStore is the acl store. ACLManager *aclstore.Manager } // New creates a new Authorizer for authorizing identity server // operations. func New(params Params) (*Authorizer, error) { for acl, users := range aclDefaults { if err := params.ACLManager.CreateACL(context.Background(), acl, users...); err != nil { return nil, errgo.Mask(err) } } a := &Authorizer{ adminPassword: params.AdminPassword, location: params.Location, store: params.Store, aclManager: params.ACLManager, } resolvers := make(map[string]groupResolver) for _, idp := range params.IdentityProviders { idp := idp resolvers[idp.Name()] = idpGroupResolver{idp} } // Add a group resolver for the built-in candid provider. resolvers["idm"] = candidGroupResolver{ store: params.Store, resolvers: resolvers, } a.groupResolvers = resolvers a.checker = identchecker.NewChecker(identchecker.CheckerParams{ Checker: NewChecker(a), Authorizer: identchecker.ACLAuthorizer{ GetACL: func(ctx context.Context, op bakery.Op) ([]string, bool, error) { return a.aclForOp(ctx, op) }, }, IdentityClient: a, MacaroonVerifier: params.MacaroonVerifier, }) return a, nil } func (a *Authorizer) aclForOp(ctx context.Context, op bakery.Op) (acl []string, public bool, _ error) { kind, name := splitEntity(op.Entity) switch kind { case kindGlobal: if name != "" { return nil, false, nil } switch op.Action { case ActionRead: acl, err := a.aclManager.ACL(ctx, readUserACL) return acl, false, errgo.Mask(err) case ActionDischargeFor: acl, err := a.aclManager.ACL(ctx, dischargeForUserACL) return acl, false, errgo.Mask(err) case ActionVerify: // Everyone is allowed to verify a macaroon. return []string{identchecker.Everyone}, true, nil case ActionLogin: // Everyone is allowed to log in. return []string{identchecker.Everyone}, true, nil case ActionDischarge: // Everyone is allowed to discharge, but they must authenticate themselves // first. return []string{identchecker.Everyone}, false, nil case ActionCreateAgent: // Anyone can create an agent, as long as they've authenticated // themselves. return []string{identchecker.Everyone}, false, nil case ActionCreateParentAgent: acl, err := a.aclManager.ACL(ctx, writeUserACL) return acl, false, errgo.Mask(err) case ActionClearUserMFACredentials: acl, err := a.aclManager.ACL(ctx, clearUserMFACredentialsACL) return acl, false, errgo.Mask(err) } case kindUser: if name == "" { return nil, false, nil } username := name switch op.Action { case ActionRead: acl, err := a.aclManager.ACL(ctx, readUserACL) return append(acl, username), false, errgo.Mask(err) case ActionReadAdmin: acl, err := a.aclManager.ACL(ctx, readUserACL) return acl, false, errgo.Mask(err) case ActionWriteAdmin: acl, err := a.aclManager.ACL(ctx, writeUserACL) return acl, false, errgo.Mask(err) case ActionReadGroups: acl, err := a.aclManager.ACL(ctx, readUserGroupsACL) return append(acl, username), false, errgo.Mask(err) case ActionWriteGroups: acl, err := a.aclManager.ACL(ctx, writeUserACL) return acl, false, errgo.Mask(err) case ActionReadSSHKeys: acl, err := a.aclManager.ACL(ctx, readUserSSHKeysACL) return append(acl, username), false, errgo.Mask(err) case ActionWriteSSHKeys: acl, err := a.aclManager.ACL(ctx, writeUserSSHKeysACL) return append(acl, username), false, errgo.Mask(err) } case kindUserID: if name == "" { return nil, false, nil } var acl []string id := store.Identity{ ProviderID: store.ProviderIdentity(name), } sterr := a.store.Identity(ctx, &id) if sterr == nil { acl = append(acl, id.Username) } if errgo.Cause(sterr) == store.ErrNotFound { // If we can't find the user, then supress the // error, whatever operation is being performed // will undoubtedly get the same error. sterr = nil } switch op.Action { case ActionRead: acl1, err := a.aclManager.ACL(ctx, readUserACL) if err == nil { err = sterr } return append(acl, acl1...), false, errgo.Mask(err) case ActionReadGroups: acl1, err := a.aclManager.ACL(ctx, readUserGroupsACL) if err == nil { err = sterr } return append(acl, acl1...), false, errgo.Mask(err) } case "groups": switch op.Action { case ActionDischarge: return strings.Fields(name), true, nil } } logger.Infof("no ACL found for op %#v", op) return nil, false, nil } // SetAdminPublicKey configures the public key on the admin user. This is // to allow agent login as the admin user. func (a *Authorizer) SetAdminPublicKey(ctx context.Context, pk *bakery.PublicKey) error { var pks []bakery.PublicKey if pk != nil { pks = append(pks, *pk) } return errgo.Mask(a.store.UpdateIdentity( ctx, &store.Identity{ ProviderID: AdminProviderID, Username: AdminUsername, PublicKeys: pks, }, store.Update{ store.Username: store.Set, store.Groups: store.Set, store.PublicKeys: store.Set, }, )) } // Auth checks that client, as identified by the given context and // macaroons, is authorized to perform the given operations. It may // return an bakery.DischargeRequiredError when further checks are // required, or params.ErrUnauthorized if the user is authenticated but // does not have the required authorization. func (a *Authorizer) Auth(ctx context.Context, mss []macaroon.Slice, ops ...bakery.Op) (*identchecker.AuthInfo, error) { authInfo, err := a.checker.Auth(mss...).Allow(ctx, ops...) if err != nil { if errgo.Cause(err) == bakery.ErrPermissionDenied { return nil, errgo.WithCausef(err, params.ErrUnauthorized, "") } return nil, errgo.Mask(err, isDischargeRequiredError) } return authInfo, nil } func isDischargeRequiredError(err error) bool { _, ok := errgo.Cause(err).(*bakery.DischargeRequiredError) return ok } // Identity creates a new identity for the user identified by the given // store.Identity. func (a *Authorizer) Identity(ctx context.Context, id *store.Identity) (*Identity, error) { aid := &Identity{ Identity: *id, authorizer: a, } if aid.Identity.ID == "" { // Get the complete identity information from the store. if err := a.store.Identity(ctx, &aid.Identity); err != nil { if errgo.Cause(err) == store.ErrNotFound { return nil, errgo.WithCausef(err, params.ErrNotFound, "") } return nil, errgo.Mask(err) } } return aid, nil } // IdentityFromContext implements // identchecker.IdentityClient.IdentityFromContext by looking for admin // credentials in the context. func (a *Authorizer) IdentityFromContext(ctx context.Context) (identchecker.Identity, []checkers.Caveat, error) { if username := usernameFromContext(ctx); username != "" { if err := CheckUserDomain(ctx, username); err != nil { return nil, nil, errgo.Mask(err) } id, err := a.Identity(ctx, &store.Identity{ Username: username, }) if err != nil { return nil, nil, errgo.Mask(err, errgo.Is(params.ErrNotFound)) } return id, nil, nil } if username, password, ok := userCredentialsFromContext(ctx); ok { // TODO the mismatch between the username in the basic auth // credentials and the admin username is unfortunate but we'll // leave it for now. We should probably remove basic-auth authentication // entirely. if username+"@candid" == AdminUsername && a.adminPassword != "" && password == a.adminPassword { id, err := a.Identity(ctx, &store.Identity{ Username: AdminUsername, }) if err == nil { return id, nil, nil } return nil, nil, errgo.Mask(err, errgo.Is(params.ErrNotFound)) } return nil, nil, errgo.WithCausef(nil, params.ErrUnauthorized, "invalid credentials") } return nil, []checkers.Caveat{ checkers.NeedDeclaredCaveat( checkers.Caveat{ Location: a.location, Condition: "is-authenticated-user", }, "username", ), }, nil } // CheckUserDomain checks that the given user name has // a valid domain name with respect to the given context // (see also ContextWithRequiredDomain). func CheckUserDomain(ctx context.Context, username string) error { domain, ok := ctx.Value(requiredDomainKey).(string) if ok && !strings.HasSuffix(username, "@"+domain) { return errgo.Newf("%q not in required domain %q", username, domain) } return nil } // DeclaredIdentity implements identchecker.IdentityClient.DeclaredIdentity by // retrieving the user information from the declared map. func (a *Authorizer) DeclaredIdentity(ctx context.Context, declared map[string]string) (identchecker.Identity, error) { username, hasUsername := declared["username"] userID, hasUserID := declared["userid"] if hasUsername == hasUserID { if hasUsername { return nil, errgo.New("both username and userid declared") } return nil, errgo.Newf("no declared user") } id, err := a.Identity(ctx, &store.Identity{ ProviderID: store.ProviderIdentity(userID), Username: username, }) if err != nil { return nil, errgo.Mask(err, errgo.Is(params.ErrNotFound)) } if err := CheckUserDomain(ctx, id.Username); err != nil { return nil, errgo.Mask(err) } return id, nil } // An Identity is the implementation of identchecker.Identity used in the // identity server. type Identity struct { store.Identity authorizer *Authorizer resolvedGroups []string } // Id implements identchecker.Identity.Id. func (id *Identity) Id() string { return string(id.Username) } // Domain implements identchecker.Identity.Domain. func (id *Identity) Domain() string { return "" } // Allow implements identchecker.ACLIdentity.Allow by checking whether the // given identity is in any of the required groups or users. func (id *Identity) Allow(ctx context.Context, acl []string) (bool, error) { if ok, isTrivial := trivialAllow(id.Username, acl); isTrivial { return ok, nil } groups, err := id.Groups(ctx) if err != nil { return false, errgo.Mask(err) } for _, a := range acl { for _, g := range groups { if g == a { return true, nil } } } return false, nil } // Groups returns all the groups associated with the user. The groups // include those stored in the identity server's database along with any // retrieved by the relevent identity provider's GetGroups method. Once // the set of groups has been determined it is cached in the Identity. func (id *Identity) Groups(ctx context.Context) ([]string, error) { if id.resolvedGroups != nil { return id.resolvedGroups, nil } groups := id.Identity.Groups if gr := id.authorizer.groupResolvers[id.ProviderID.Provider()]; gr != nil { var err error groups, err = gr.resolveGroups(ctx, &id.Identity) if err != nil { logger.Warningf("error resolving groups: %s", err) } else { id.resolvedGroups = groups } } return groups, nil } // trivialAllow reports whether the username should be allowed // access to the given ACL based on a superficial inspection // of the ACL. If there is a definite answer, it will return // a true isTrivial; otherwise it will return (false, false). func trivialAllow(username string, acl []string) (allow, isTrivial bool) { if len(acl) == 0 { return false, true } for _, name := range acl { if name == "everyone" || name == username { return true, true } } return false, false } // DomainDischargeOp creates an operation that is discharging the // specified domain. func DomainDischargeOp(domain string) bakery.Op { return op("domain-"+domain, ActionDischarge) } // GroupsDischargeOp creates an operation that is discharging as a user // in one of the specified groups. func GroupsDischargeOp(groups []string) bakery.Op { return op("groups-"+strings.Join(groups, " "), ActionDischarge) } // UserOp is an operation specific to a username. func UserOp(u params.Username, action string) bakery.Op { return op(kindUser+"-"+string(u), action) } // UserIDOp is an operation specific to a user ID. func UserIDOp(uid string, action string) bakery.Op { return op(kindUserID+"-"+uid, action) } // GlobalOp is an operation that is not specific to a user. func GlobalOp(action string) bakery.Op { return op(kindGlobal, action) } func op(entity, action string) bakery.Op { return bakery.Op{ Entity: entity, Action: action, } } func splitEntity(entity string) (string, string) { if i := strings.Index(entity, "-"); i > 0 { return entity[0:i], entity[i+1:] } return entity, "" } // A groupResolver is used to update the groups associated with an // identity. type groupResolver interface { // resolveGroups returns the group information for the given // identity. If a non-nil error is returned it will be logged, // but the returned list of groups will still be taken as the set // of groups to be associated with the identity. resolveGroups(context.Context, *store.Identity) ([]string, error) } // candidGroupResolver is the group resolver used for identities using // the "idm" provider type. These are the agent identites. type candidGroupResolver struct { store store.Store resolvers map[string]groupResolver } // resolveGroups implements groupResolver by checking that the groups // allocated to the agent are still valid. All groups listed in the // agent's identity are checked with the owner and only those where the // owner is also still a member are returned. The result is effectively // the union between the agent's groups and the owner's groups. func (r candidGroupResolver) resolveGroups(ctx context.Context, identity *store.Identity) ([]string, error) { if identity.Owner == "" { // No owner implies a parent agent. These agents are // members of only the specified groups. return identity.Groups, nil } if identity.Owner == AdminProviderID { // The admin user is a member of all groups by definition. return identity.Groups, nil } ownerIdentity := store.Identity{ ProviderID: identity.Owner, } if err := r.store.Identity(ctx, &ownerIdentity); err != nil { if errgo.Cause(err) != store.ErrNotFound { return nil, errgo.Mask(err) } return nil, nil } resolver := r.resolvers[identity.Owner.Provider()] if resolver == nil { // Owner is somehow in an unknown provider. // TODO log/return an error? return nil, nil } ownerGroups, err := resolver.resolveGroups(ctx, &ownerIdentity) if err != nil { return nil, errgo.Mask(err) } allowedGroups := make([]string, 0, len(identity.Groups)) for _, g1 := range identity.Groups { for _, g2 := range ownerGroups { if g2 == g1 { allowedGroups = append(allowedGroups, g1) break } } } return allowedGroups, nil } type idpGroupResolver struct { idp idp.IdentityProvider } // resolveGroups implements groupResolver by getting the groups from the // idp and adding them to the set stored in the identity server. func (r idpGroupResolver) resolveGroups(ctx context.Context, id *store.Identity) ([]string, error) { groups, err := r.idp.GetGroups(ctx, id) for i, g := range groups { groups[i] = groupWithDomain(g, r.idp.Domain()) } if err != nil { // We couldn't get the groups, so return only those stored in the database. return id.Groups, errgo.Mask(err) } return uniqueStrings(append(groups, id.Groups...)), nil } // groupWithDomain adds the given domain to the group name, if it is // non-zero. func groupWithDomain(group, domain string) string { if domain == "" { return group } return group + "@" + domain } // uniqueStrings removes all duplicates from the supplied // string slice, updating the slice in place. // The values will be in lexicographic order. func uniqueStrings(ss []string) []string { if len(ss) < 2 { return ss } sort.Strings(ss) prev := ss[0] out := ss[:1] for _, s := range ss[1:] { if s == prev { continue } out = append(out, s) prev = s } return out } golang-github-canonical-candid-1.12.3/internal/auth/auth_test.go000066400000000000000000000303751457263123000246110ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package auth_test import ( "context" "fmt" "sort" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/juju/aclstore/v2" errgo "gopkg.in/errgo.v1" macaroon "gopkg.in/macaroon.v2" "github.com/canonical/candid/candidclient" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/static" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) func TestAuth(t *testing.T) { qtsuite.Run(qt.New(t), &authSuite{}) } type authSuite struct { store *candidtest.Store oven *bakery.Oven authorizer *auth.Authorizer context context.Context adminAgentKey *bakery.KeyPair } const identityLocation = "https://identity.test/id" func (s *authSuite) Init(c *qt.C) { s.store = candidtest.NewStore() key, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) locator := bakery.NewThirdPartyStore() locator.AddInfo(identityLocation, bakery.ThirdPartyInfo{ PublicKey: key.Public, Version: bakery.LatestVersion, }) s.oven = bakery.NewOven(bakery.OvenParams{ Key: key, Locator: locator, Location: "identity", }) aclManager, err := aclstore.NewManager(context.Background(), aclstore.Params{ Store: s.store.ACLStore, InitialAdminUsers: []string{auth.AdminUsername}, }) c.Assert(err, qt.IsNil) ctx, close := s.store.Store.Context(context.Background()) c.Defer(close) s.context = ctx s.authorizer, err = auth.New(auth.Params{ AdminPassword: "password", Location: identityLocation, MacaroonVerifier: s.oven, Store: s.store.Store, IdentityProviders: []idp.IdentityProvider{ static.NewIdentityProvider(static.Params{ Name: "test", Users: map[string]static.UserInfo{ "testuser": { Password: "testpass", Groups: []string{"somegroup"}, }, }, }), }, ACLManager: aclManager, }) c.Assert(err, qt.IsNil) s.adminAgentKey, err = bakery.GenerateKey() c.Assert(err, qt.IsNil) err = s.authorizer.SetAdminPublicKey(s.context, &s.adminAgentKey.Public) c.Assert(err, qt.IsNil) } func (s *authSuite) createIdentity(c *qt.C, username string, pk *bakery.PublicKey, groups ...string) *auth.Identity { var pks []bakery.PublicKey if pk != nil { pks = append(pks, *pk) } err := s.store.Store.UpdateIdentity(s.context, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", username), Username: username, Groups: groups, PublicKeys: pks, }, store.Update{ store.Username: store.Set, store.Groups: store.Set, store.PublicKeys: store.Set, }) c.Assert(err, qt.IsNil) id, err := s.authorizer.Identity(s.context, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", username), }) c.Assert(err, qt.IsNil) return id } func (s authSuite) identityMacaroon(c *qt.C, username string) *bakery.Macaroon { m, err := s.oven.NewMacaroon( s.context, bakery.LatestVersion, []checkers.Caveat{ candidclient.UserDeclaration(username), }, identchecker.LoginOp, ) c.Assert(err, qt.IsNil) return m } func (s *authSuite) TestAuthorizeWithAdminCredentials(c *qt.C) { tests := []struct { about string username string password string expectErrorMessage string }{{ about: "good credentials", username: "admin", password: "password", }, { about: "bad username", username: "not-admin", password: "password", expectErrorMessage: "could not determine identity: invalid credentials", }, { about: "bad password", username: "admin", password: "not-password", expectErrorMessage: "could not determine identity: invalid credentials", }} for _, test := range tests { c.Run(test.about, func(c *qt.C) { ctx := context.Background() if test.username != "" { ctx = auth.ContextWithUserCredentials(ctx, test.username, test.password) } authInfo, err := s.authorizer.Auth(ctx, nil, identchecker.LoginOp) if test.expectErrorMessage != "" { c.Assert(err, qt.ErrorMatches, test.expectErrorMessage) c.Assert(errgo.Cause(err), qt.Equals, params.ErrUnauthorized) return } c.Assert(err, qt.IsNil) c.Assert(authInfo.Identity.Id(), qt.Equals, auth.AdminUsername) }) } } func (s *authSuite) TestUserHasPublicKeyCaveat(c *qt.C) { key, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) cav := auth.UserHasPublicKeyCaveat(params.Username("test"), &key.Public) c.Assert(cav.Namespace, qt.Equals, auth.CheckersNamespace) c.Assert(cav.Condition, qt.Matches, "user-has-public-key test .*") c.Assert(cav.Location, qt.Equals, "") } func (s *authSuite) TestUserHasPublicKeyChecker(c *qt.C) { key, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) ctx, close := s.store.Store.Context(context.Background()) defer close() s.createIdentity(c, "test-user", &key.Public) checker := auth.NewChecker(s.authorizer) checkCaveat := func(cav checkers.Caveat) error { cav = checker.Namespace().ResolveCaveat(cav) return checker.CheckFirstPartyCaveat(ctx, cav.Condition) } err = checkCaveat(auth.UserHasPublicKeyCaveat(params.Username("test-user"), &key.Public)) c.Assert(err, qt.IsNil) // Unknown username err = checkCaveat(auth.UserHasPublicKeyCaveat("test2", &key.Public)) c.Assert(err, qt.ErrorMatches, "caveat.*not satisfied: public key not valid for user") // Incorrect public key err = checkCaveat(auth.UserHasPublicKeyCaveat("test2", new(bakery.PublicKey))) c.Assert(err, qt.ErrorMatches, "caveat.*not satisfied: public key not valid for user") // Invalid argument err = checkCaveat(checkers.Caveat{ Namespace: auth.CheckersNamespace, Condition: "user-has-public-key test", }) c.Assert(err, qt.ErrorMatches, "caveat.*not satisfied: caveat badly formatted") // Invalid public key err = checkCaveat(checkers.Caveat{ Namespace: auth.CheckersNamespace, Condition: "user-has-public-key test " + key.Public.String()[1:], }) c.Assert(err, qt.ErrorMatches, `caveat.*not satisfied: invalid public key ".*": .*`) } var aclForOpTests = []struct { op bakery.Op expect []string expectPublic bool }{{ op: op("other", "read"), }, { op: auth.GlobalOp("read"), expect: []string{auth.AdminUsername, auth.UserInformationGroup}, expectPublic: false, }, { op: auth.GlobalOp("verify"), expect: []string{identchecker.Everyone}, expectPublic: true, }, { op: auth.GlobalOp("dischargeFor"), expect: []string{auth.AdminUsername}, expectPublic: false, }, { op: auth.GlobalOp("login"), expect: []string{identchecker.Everyone}, expectPublic: true, }, { op: auth.GlobalOp("createAgent"), expect: []string{identchecker.Everyone}, }, { op: op("global-foo", "login"), }, { op: auth.GlobalOp("unknown"), }, { op: op("u", "read"), }, { op: auth.UserOp("", "read"), }, { op: auth.UserOp("bob", "read"), expect: []string{"bob", auth.AdminUsername, auth.UserInformationGroup}, }, { op: auth.UserOp("bob", "readAdmin"), expect: []string{auth.AdminUsername, auth.UserInformationGroup}, }, { op: auth.UserOp("bob", "writeAdmin"), expect: []string{auth.AdminUsername}, }, { op: auth.UserOp("bob", "readGroups"), expect: []string{"bob", auth.AdminUsername, auth.GroupListGroup, auth.UserInformationGroup}, }, { op: auth.UserOp("bob", "writeGroups"), expect: []string{auth.AdminUsername}, }, { op: auth.UserOp("bob", "readSSHKeys"), expect: []string{"bob", auth.AdminUsername, auth.SSHKeyGetterGroup, auth.UserInformationGroup}, }, { op: auth.UserOp("bob", "writeSSHKeys"), expect: []string{"bob", auth.AdminUsername}, }} func (s *authSuite) TestACLForOp(c *qt.C) { for _, test := range aclForOpTests { c.Run(fmt.Sprintf("%s-%s", test.op.Entity, test.op.Action), func(c *qt.C) { sort.Strings(test.expect) acl, public, err := auth.AuthorizerACLForOp(s.authorizer, context.Background(), test.op) c.Assert(err, qt.IsNil) sort.Strings(acl) c.Assert(acl, qt.DeepEquals, test.expect) c.Assert(public, qt.Equals, test.expectPublic) }) } } func (s *authSuite) TestAdminUserGroups(c *qt.C) { ctx := auth.ContextWithUserCredentials(context.Background(), "admin", "password") authInfo, err := s.authorizer.Auth(ctx, nil, identchecker.LoginOp) c.Assert(err, qt.IsNil) assertAuthorizedGroups(c, authInfo, nil) } func (s *authSuite) TestNonExistentUser(c *qt.C) { m := s.identityMacaroon(c, "noone") _, err := s.authorizer.Auth(s.context, []macaroon.Slice{{m.M()}}, identchecker.LoginOp) c.Assert(err, qt.ErrorMatches, `could not determine identity: user noone not found`) } func (s *authSuite) TestExistingUserGroups(c *qt.C) { // good identity s.createIdentity(c, "test", nil, "test-group1", "test-group2") m := s.identityMacaroon(c, "test") authInfo, err := s.authorizer.Auth(s.context, []macaroon.Slice{{m.M()}}, identchecker.LoginOp) c.Assert(err, qt.IsNil) assertAuthorizedGroups(c, authInfo, []string{"test-group1", "test-group2"}) } func assertAuthorizedGroups(c *qt.C, authInfo *identchecker.AuthInfo, expectGroups []string) { c.Assert(authInfo.Identity, qt.Not(qt.IsNil)) ident := authInfo.Identity.(*auth.Identity) groups, err := ident.Groups(context.Background()) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, expectGroups) } var identityAllowTests = []struct { about string // groups holds the groups the user is a member of. groups []string // externalGroups holds the groups that will // be returned by the external group getter. externalGroups []string // externalGroupsError holds an error to be returned by externalGroups. externalGroupsError error // ACL holds the ACL that's being checked. acl []string // expectAllowed holds whether the access to the ACL // should be granted. expectAllowed bool // expectError holds the expected error from the Allow call. expectError string }{{ about: "everyone is allowed even with no store", acl: []string{"everyone"}, expectAllowed: true, }, { about: "user is allowed even with no store", acl: []string{"testuser"}, expectAllowed: true, }, { about: "empty ACL doesn't require store", expectAllowed: false, }, { about: "user is allowed if they're in the expected group internally", acl: []string{"somegroup"}, groups: []string{"x", "somegroup"}, expectAllowed: true, }, { about: "user is allowed if they're in the expected group externally", acl: []string{"somegroup"}, expectAllowed: true, }, { about: "user is not allowed if they're not in the expected group", acl: []string{"othergroup"}, groups: []string{"somegroup"}, expectAllowed: false, }} func (s *authSuite) TestIdentityAllow(c *qt.C) { for _, test := range identityAllowTests { c.Run(test.about, func(c *qt.C) { id := s.createIdentity(c, "testuser", nil, test.groups...) ok, err := id.Allow(s.context, test.acl) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) c.Assert(ok, qt.Equals, false) } else { c.Assert(err, qt.IsNil) c.Assert(ok, qt.Equals, test.expectAllowed) } }) } } func (s *authSuite) TestAuthorizeMacaroonRequired(c *qt.C) { authInfo, err := s.authorizer.Auth(s.context, nil, identchecker.LoginOp) c.Assert(err, qt.ErrorMatches, `macaroon discharge required: authentication required`) c.Assert(authInfo, qt.IsNil) cause := errgo.Cause(err) derr, ok := cause.(*bakery.DischargeRequiredError) if !ok { c.Fatalf("error %#v (cause type %T) is not DischargeRequiredError", err, cause) } c.Assert(derr.Ops, qt.DeepEquals, []bakery.Op{identchecker.LoginOp}) c.Assert(derr.Caveats, qt.DeepEquals, []checkers.Caveat{{Condition: "need-declared username is-authenticated-user", Location: "https://identity.test/id"}}) } func op(entity, action string) bakery.Op { return bakery.Op{ Entity: entity, Action: action, } } type testGroupGetter struct { groups []string error error } func (t testGroupGetter) GetGroups(_ context.Context, id *store.Identity) ([]string, error) { return t.groups, t.error } golang-github-canonical-candid-1.12.3/internal/auth/checkers.go000066400000000000000000000043231457263123000243720ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package auth import ( "bytes" "context" "strings" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "gopkg.in/errgo.v1" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) const ( checkersNamespace = "jujucharms.com/identity" userHasPublicKeyCondition = "user-has-public-key" ) // Namespace contains the checkers.Namespace supported by the identity // service. var Namespace = checkers.NewNamespace(map[string]string{ checkers.StdNamespace: "", httpbakery.CheckersNamespace: "http", checkersNamespace: "", }) func NewChecker(a *Authorizer) *checkers.Checker { checker := httpbakery.NewChecker() checker.Namespace().Register(checkersNamespace, "") checker.Register(userHasPublicKeyCondition, checkersNamespace, a.checkUserHasPublicKey) return checker } // UserHasPublicKeyCaveat creates a first-party caveat that ensures that // the given user is associated with the given public key. func UserHasPublicKeyCaveat(user params.Username, pk *bakery.PublicKey) checkers.Caveat { return checkers.Caveat{ Namespace: checkersNamespace, Condition: checkers.Condition(userHasPublicKeyCondition, string(user)+" "+pk.String()), } } // checkUserHasPublicKey checks the "user-has-public-key" caveat. func (a *Authorizer) checkUserHasPublicKey(ctx context.Context, cond, arg string) error { parts := strings.Fields(arg) if len(parts) != 2 { return errgo.New("caveat badly formatted") } var publicKey bakery.PublicKey if err := publicKey.UnmarshalText([]byte(parts[1])); err != nil { return errgo.Notef(err, "invalid public key %q", parts[1]) } identity := store.Identity{ Username: parts[0], } if err := a.store.Identity(ctx, &identity); err != nil { if errgo.Cause(err) != store.ErrNotFound { return errgo.Mask(err) } return errgo.Newf("public key not valid for user") } for _, pk := range identity.PublicKeys { if bytes.Equal(pk.Key[:], publicKey.Key[:]) { return nil } } return errgo.Newf("public key not valid for user") } golang-github-canonical-candid-1.12.3/internal/auth/context.go000066400000000000000000000040541457263123000242700ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package auth import ( "context" ) type contextKey int const ( userCredentialsKey contextKey = iota requiredDomainKey dischargeIDKey usernameKey ) type userCredentials struct { username, password string } // ContextWithUserCredentials returns a context with the given user // credentials attached. These will then be checked when performing // authorizations. func ContextWithUserCredentials(ctx context.Context, username, password string) context.Context { return context.WithValue(ctx, userCredentialsKey, userCredentials{username, password}) } func userCredentialsFromContext(ctx context.Context) (username, password string, ok bool) { uc, ok := ctx.Value(userCredentialsKey).(userCredentials) return uc.username, uc.password, ok } // ContextWithRequiredDomain returns a context associated // with the given domain, such that declared identities // will only be allowed if they have that domain. func ContextWithRequiredDomain(ctx context.Context, domain string) context.Context { return context.WithValue(ctx, requiredDomainKey, domain) } func requiredDomainFromContext(ctx context.Context) string { requiredDomain, _ := ctx.Value(usernameKey).(string) return requiredDomain } // ContextWithDischargeID returns a context with the given discharge ID // stored. func ContextWithDischargeID(ctx context.Context, dischargeID string) context.Context { return context.WithValue(ctx, dischargeIDKey, dischargeID) } func dischargeIDFromContext(ctx context.Context) string { dischargeID, _ := ctx.Value(dischargeIDKey).(string) return dischargeID } // ContextWithUsername returns a context with the given username stored. // Any user attached to the context will be considered authenticated by // IdentityFromContext. func ContextWithUsername(ctx context.Context, username string) context.Context { return context.WithValue(ctx, usernameKey, username) } func usernameFromContext(ctx context.Context) string { username, _ := ctx.Value(usernameKey).(string) return username } golang-github-canonical-candid-1.12.3/internal/auth/export_test.go000066400000000000000000000003161457263123000251610ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package auth var ( AuthorizerACLForOp = (*Authorizer).aclForOp ) const CheckersNamespace = checkersNamespace golang-github-canonical-candid-1.12.3/internal/auth/httpauth/000077500000000000000000000000001457263123000241135ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/internal/auth/httpauth/httpauth.go000066400000000000000000000044601457263123000263070ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package httpauth import ( "context" "net/http" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/params" ) // An Authorizer is used to authorize HTTP requests. type Authorizer struct { authorizer *auth.Authorizer oven *bakery.Oven timeout time.Duration } // New creates a new Authorizer for authorizing HTTP requests made to the // identity server. The given oven is used to make new macaroons; the // given authorizer is used as the underlying authorizer. func New(o *bakery.Oven, a *auth.Authorizer, timeout time.Duration) *Authorizer { return &Authorizer{ authorizer: a, oven: o, timeout: timeout, } } // Auth checks that client making the given request is authorized to // perform the given operations. It may return an httpbakery error when // further checks are required, or params.ErrUnauthorized if the user is // authenticated but does not have the required authorization. func (a *Authorizer) Auth(ctx context.Context, req *http.Request, ops ...bakery.Op) (*identchecker.AuthInfo, error) { ctx = httpbakery.ContextWithRequest(ctx, req) if username, password, ok := req.BasicAuth(); ok { ctx = auth.ContextWithUserCredentials(ctx, username, password) } authInfo, err := a.authorizer.Auth(ctx, httpbakery.RequestMacaroons(req), ops...) if err == nil { return authInfo, nil } derr, ok := errgo.Cause(err).(*bakery.DischargeRequiredError) if !ok { return nil, errgo.Mask(err, errgo.Is(params.ErrUnauthorized)) } caveats := append(derr.Caveats, checkers.TimeBeforeCaveat(time.Now().Add(a.timeout))) m, err := a.oven.NewMacaroon( ctx, httpbakery.RequestVersion(req), caveats, derr.Ops..., ) if err != nil { return nil, errgo.Notef(err, "cannot create macaroon") } return nil, httpbakery.NewDischargeRequiredError(httpbakery.DischargeRequiredErrorParams{ Macaroon: m, Request: req, OriginalError: derr, CookieNameSuffix: "candid", }) } golang-github-canonical-candid-1.12.3/internal/auth/httpauth/httpauth_test.go000066400000000000000000000110151457263123000273400ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package httpauth_test import ( "context" "encoding/base64" "net/http" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/aclstore/v2" "gopkg.in/errgo.v1" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/internal/auth/httpauth" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/params" ) type authSuite struct { store *candidtest.Store oven *bakery.Oven aclManager *aclstore.Manager } func TestAuth(t *testing.T) { qtsuite.Run(qt.New(t), &authSuite{}) } const identityLocation = "https://identity.test/id" func (s *authSuite) Init(c *qt.C) { s.store = candidtest.NewStore() key, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) locator := bakery.NewThirdPartyStore() locator.AddInfo(identityLocation, bakery.ThirdPartyInfo{ PublicKey: key.Public, Version: bakery.LatestVersion, }) s.oven = bakery.NewOven(bakery.OvenParams{ Key: key, Locator: locator, Location: "identity", }) s.aclManager, err = aclstore.NewManager(context.Background(), aclstore.Params{ Store: s.store.ACLStore, InitialAdminUsers: []string{auth.AdminUsername}, }) c.Assert(err, qt.IsNil) } func (s *authSuite) TestAuthorizeWithAdminCredentials(c *qt.C) { tests := []struct { about string adminPassword string header http.Header expectErrorMessage string }{{ about: "good credentials", adminPassword: "open sesame", header: http.Header{ "Authorization": []string{"Basic " + b64str("admin:open sesame")}, }, }, { about: "bad username", adminPassword: "open sesame", header: http.Header{ "Authorization": []string{"Basic " + b64str("xadmin:open sesame")}, }, expectErrorMessage: "could not determine identity: invalid credentials", }, { about: "bad password", adminPassword: "open sesame", header: http.Header{ "Authorization": []string{"Basic " + b64str("admin:open sesam")}, }, expectErrorMessage: "could not determine identity: invalid credentials", }, { about: "empty password denies access", adminPassword: "", header: http.Header{ "Authorization": []string{"Basic " + b64str("admin:")}, }, expectErrorMessage: "could not determine identity: invalid credentials", }} for _, test := range tests { c.Run(test.about, func(c *qt.C) { authorizer, err := auth.New(auth.Params{ AdminPassword: test.adminPassword, Location: identityLocation, Store: s.store.Store, MacaroonVerifier: s.oven, ACLManager: s.aclManager, }) c.Assert(err, qt.IsNil) err = authorizer.SetAdminPublicKey(context.Background(), &bakery.PublicKey{}) c.Assert(err, qt.IsNil) httpAuthorizer := httpauth.New(s.oven, authorizer, 0) req, _ := http.NewRequest("GET", "/", nil) for attr, val := range test.header { req.Header[attr] = val } authInfo, err := httpAuthorizer.Auth(context.Background(), req, identchecker.LoginOp) if test.expectErrorMessage != "" { c.Assert(err, qt.ErrorMatches, test.expectErrorMessage) c.Assert(errgo.Cause(err), qt.Equals, params.ErrUnauthorized) return } c.Assert(err, qt.IsNil) c.Assert(authInfo.Identity.Id(), qt.Equals, auth.AdminUsername) }) } } func (s *authSuite) TestAuthorizeMacaroonRequired(c *qt.C) { authorizer, err := auth.New(auth.Params{ AdminPassword: "open sesame", Location: identityLocation, Store: s.store.Store, MacaroonVerifier: s.oven, ACLManager: s.aclManager, }) httpAuthorizer := httpauth.New(s.oven, authorizer, 0) req, err := http.NewRequest("GET", "http://example.com/v1/test", nil) c.Assert(err, qt.IsNil) authInfo, err := httpAuthorizer.Auth(context.Background(), req, identchecker.LoginOp) c.Assert(err, qt.ErrorMatches, `macaroon discharge required: authentication required`) c.Assert(authInfo, qt.IsNil) derr, ok := errgo.Cause(err).(*httpbakery.Error) if !ok { c.Fatalf("error %#v is not httpbakery.Error", err) } c.Assert(derr.Info.CookieNameSuffix, qt.Equals, "candid") c.Assert(derr.Info.MacaroonPath, qt.Equals, "../") c.Assert(derr.Info.Macaroon, qt.Not(qt.IsNil)) } func b64str(s string) string { return base64.StdEncoding.EncodeToString([]byte(s)) } golang-github-canonical-candid-1.12.3/internal/candidtest/000077500000000000000000000000001457263123000234335ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/internal/candidtest/discharge.go000066400000000000000000000210431457263123000257130ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package candidtest provides suites and functions useful for testing the // identity manager. package candidtest import ( "bytes" "context" "io/ioutil" "net/http" "net/http/cookiejar" "net/url" "time" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" errgo "gopkg.in/errgo.v1" macaroon "gopkg.in/macaroon.v2" ) // DischargeCreator represents a third party service // that creates discharges addressed to Candid. type DischargeCreator struct { ServerURL string Bakery *identchecker.Bakery bakeryKey *bakery.KeyPair } // NewDischargeCreator returns a DischargeCreator that // creates third party caveats addressed to the given server, // which must be serving the "discharger" API. func NewDischargeCreator(server *Server) *DischargeCreator { bakeryKey, err := bakery.GenerateKey() if err != nil { panic(err) } return &DischargeCreator{ ServerURL: server.URL, Bakery: identchecker.NewBakery(identchecker.BakeryParams{ Locator: server, Key: bakeryKey, IdentityClient: server.AdminIdentityClient(false), Location: "discharge-test", }), bakeryKey: bakeryKey, } } // NewUserIDDischargeCreator returns a DischargeCreator that creates // third party caveats addressed to the given server, which must be // serving the "discharger" API. The macaroons will use unique user IDs // rather than usernames. func NewUserIDDischargeCreator(server *Server) *DischargeCreator { bakeryKey, err := bakery.GenerateKey() if err != nil { panic(err) } return &DischargeCreator{ ServerURL: server.URL, Bakery: identchecker.NewBakery(identchecker.BakeryParams{ Locator: server, Key: bakeryKey, IdentityClient: server.AdminIdentityClient(true), Location: "discharge-test", }), bakeryKey: bakeryKey, } } // AssertDischarge checks that a macaroon can be discharged with // interaction using the specified visitor. func (s *DischargeCreator) AssertDischarge(c *qt.C, i httpbakery.Interactor) { ms, err := s.Discharge(c, "is-authenticated-user", BakeryClient(i)) c.Assert(err, qt.Equals, nil, qt.Commentf("%s", errgo.Details(err))) _, err = s.Bakery.Checker.Auth(ms).Allow(context.Background(), identchecker.LoginOp) c.Assert(err, qt.IsNil) } // Discharge attempts to perform a discharge of a new macaroon against // this suites identity server using the given client and returns a // macaroon slice containing a discharged macaroon or any error. The // newly minted macaroon will have a third-party caveat addressed to the // identity server with the given condition. func (s *DischargeCreator) Discharge(c *qt.C, condition string, client *httpbakery.Client) (macaroon.Slice, error) { return client.DischargeAll(context.Background(), s.NewMacaroon(c, condition, identchecker.LoginOp)) } // NewMacaroon creates a new macaroon with a third-party caveat addressed // to the identity server which has the given condition. func (s *DischargeCreator) NewMacaroon(c *qt.C, condition string, op bakery.Op) *bakery.Macaroon { m, err := s.Bakery.Oven.NewMacaroon( context.Background(), bakery.LatestVersion, []checkers.Caveat{{ Location: s.ServerURL, Condition: condition, }, checkers.TimeBeforeCaveat(time.Now().Add(time.Minute))}, op, ) c.Assert(err, qt.IsNil) return m } // AssertMacaroon asserts that the given macaroon slice is valid for the // given operation. If id is specified then the declared identity in the // macaroon is checked to be the same as id. func (s *DischargeCreator) AssertMacaroon(c *qt.C, ms macaroon.Slice, op bakery.Op, id string) { ui, err := s.Bakery.Checker.Auth(ms).Allow(context.Background(), op) c.Assert(err, qt.IsNil) if id == "" { return } c.Assert(ui.Identity.Id(), qt.Equals, id) } // A VisitorFunc converts a function to a httpbakery.LegacyInteractor. type VisitorFunc func(*url.URL) error // LegacyInteract implements httpbakery.LegacyInteractor.LegacyInteract. func (f VisitorFunc) LegacyInteract(ctx context.Context, _ *httpbakery.Client, _ string, u *url.URL) error { return f(u) } // A ResponseHandler is a function that is used by OpenWebBrowser to // perform further processing with a response. A ResponseHandler should // parse the response to determine the next action, close the body of the // incoming response and perform queries in order to return the final // response to the caller. The final response should not have its body // closed. type ResponseHandler func(*http.Client, *http.Response) (*http.Response, error) // OpenWebBrowser returns a function that simulates opening a web browser // to complete a login. This function only returns a non-nil error to its // caller if there is an error initialising the client. If rh is non-nil // it will be called with the *http.Response that was received by the // client. This handler should arrange for any required further // processing and return the result. func OpenWebBrowser(c *qt.C, rh ResponseHandler) func(u *url.URL) error { return func(u *url.URL) error { jar, err := cookiejar.New(nil) if err != nil { return errgo.Mask(err) } client := &http.Client{ Jar: jar, } resp, err := client.Get(u.String()) if err != nil { c.Logf("error getting login URL %s: %s", u.String(), err) return nil } if rh != nil { resp, err = rh(client, resp) if err != nil { c.Logf("error handling login response: %s", err) return nil } } defer resp.Body.Close() if resp.StatusCode >= 400 { buf, _ := ioutil.ReadAll(resp.Body) c.Logf("interaction returned error status (%s): %s", resp.Status, buf) } return nil } } // PostLoginForm returns a ResponseHandler that can be passed to // OpenWebBrowser which will complete a login form with the given // Username and Password, and return the result. func PostLoginForm(username, password string) ResponseHandler { return func(client *http.Client, resp *http.Response) (*http.Response, error) { defer resp.Body.Close() purl, err := LoginFormAction(resp) if err != nil { return nil, errgo.Mask(err) } resp, err = client.PostForm(purl, url.Values{ "username": {username}, "password": {password}, }) return resp, errgo.Mask(err, errgo.Any) } } // SelectInteractiveLogin is a ResponseHandler that processes the list of // login methods in the incoming response and performs a GET on that URL. // If rh is non-nil it will be used to further process the response // before returning to the caller. func SelectInteractiveLogin(rh ResponseHandler) ResponseHandler { return func(client *http.Client, resp *http.Response) (*http.Response, error) { defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errgo.Newf("unexpected status %q", resp.Status) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, errgo.Mask(err) } // The body, as specified by the // authenticationRequiredTemplate, will be a list of // interactive login URLs, one on each line. Choose the // first valid one. parts := bytes.Split(body, []byte("\n")) lurl := "" for _, p := range parts { if len(p) == 0 { continue } s := string(p) if _, err := url.Parse(s); err == nil { lurl = s break } } if lurl == "" { return nil, errgo.New("login returned no URLs") } resp, err = client.Get(lurl) if err != nil { return resp, errgo.Mask(err) } if rh != nil { resp, err = rh(client, resp) } return resp, errgo.Mask(err, errgo.Any) } } // LoginFormAction gets the action parameter (POST URL) of a login form. func LoginFormAction(resp *http.Response) (string, error) { buf, err := ioutil.ReadAll(resp.Body) if err != nil { return "", errgo.Mask(err, errgo.Any) } // It is expected that the "login-form" template in this // package will have been used to generate the response. // This puts the "Action" (POST URL) parameter on the // first line by itself. parts := bytes.Split(buf, []byte("\n")) purl := string(parts[0]) if len(purl) == 0 { purl = resp.Request.URL.String() } return purl, nil } // PasswordLogin return a function that can be used with // httpbakery.WebBrowserInteractor.OpenWebBrowser that will be configured // to perform a username/password login using the given values. func PasswordLogin(c *qt.C, username, password string) func(u *url.URL) error { return OpenWebBrowser(c, SelectInteractiveLogin(PostLoginForm(username, password))) } golang-github-canonical-candid-1.12.3/internal/candidtest/logging.go000066400000000000000000000020231457263123000254050ustar00rootroot00000000000000package candidtest import ( "os" qt "github.com/frankban/quicktest" "github.com/juju/loggo" ) // LogTo configures loggo to log to qt.C for the duration // of the test. If TEST_LOGGING_CONFIG is set, it // will be used to configure the logging modules. // // When c.Done is called, the loggo configuration will // be reset. func LogTo(c *qt.C) { cfg := os.Getenv("TEST_LOGGING_CONFIG") if cfg == "" { cfg = "DEBUG" } // Don't use the default writer for the test logging, which // means we can still get logging output from tests that // replace the default writer. loggo.ResetLogging() loggo.RegisterWriter(loggo.DefaultWriterName, discardWriter{}) loggo.RegisterWriter("testlogger", &loggoWriter{c}) err := loggo.ConfigureLoggers(cfg) c.Assert(err, qt.IsNil) c.Defer(loggo.ResetLogging) } type loggoWriter struct { c *qt.C } func (w *loggoWriter) Write(entry loggo.Entry) { w.c.Logf("%s %s %s", entry.Level, entry.Module, entry.Message) } type discardWriter struct{} func (discardWriter) Write(entry loggo.Entry) { } golang-github-canonical-candid-1.12.3/internal/candidtest/server.go000066400000000000000000000233341457263123000252750ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package candidtest import ( "context" "html/template" "net/http" "net/http/httptest" "net/url" "strings" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" aclstore "github.com/juju/aclstore/v2" "github.com/juju/simplekv/memsimplekv" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/candidclient" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/internal/identity" "github.com/canonical/candid/store" ) var DefaultTemplate = template.New("") func init() { template.Must(DefaultTemplate.New("authentication-required").Parse(authenticationRequiredTemplate)) template.Must(DefaultTemplate.New("login").Parse(loginTemplate)) template.Must(DefaultTemplate.New("login-form").Parse(loginFormTemplate)) } const ( // This format is interpretted by SelectInteractiveLogin. authenticationRequiredTemplate = "{{range .IDPs}}{{.URL}}\n{{end}}" loginTemplate = "login successful as user {{.Username}}\n" loginFormTemplate = "{{.Action}}\n{{.Error}}\n" ) // Server implements a test fixture that contains a candid server. type Server struct { // URL contains the URL of the server. URL string // Ctx contains a context.Context that has been initialised with // the servers. Ctx context.Context // Key holds the key that the server uses. Key *bakery.KeyPair // params contains the parameters that were passed to identity.New. params identity.ServerParams handler *identity.Server server *httptest.Server adminAgentKey *bakery.KeyPair closeStore func() closeMeetingStore func() agentID int } // NewMemServer returns a Server instance // that uses in-memory storage and serves // the given API version func NewMemServer(c *qt.C, versions map[string]identity.NewAPIHandlerFunc) *Server { return NewServer(c, NewStore().ServerParams(), versions) } // NewServer returns new Server instance. The server parameters must // contain at least Store, MeetingStore and RootKeyStore. The versions // argument configures what API versions to serve. // // If p.Key is zero then a new key will be generated. If p.PrivateAddr // is zero then it will default to localhost. If p.Template is zero then // DefaultTemplate will be used. func NewServer(c *qt.C, p identity.ServerParams, versions map[string]identity.NewAPIHandlerFunc) *Server { return newServer(c, p, versions, "") } // NewServerWithSublocation returns a new Server instance. It does the same // as NewServer, but it allows to specify a sublocation that fakes the // server as operating from a subpath (e.g. http://serveraddr/sublocation). func NewServerWithSublocation(c *qt.C, p identity.ServerParams, versions map[string]identity.NewAPIHandlerFunc, sublocation string) *Server { return newServer(c, p, versions, sublocation) } func newServer(c *qt.C, p identity.ServerParams, versions map[string]identity.NewAPIHandlerFunc, sublocation string) *Server { s := new(Server) s.params = p if s.params.ACLStore == nil { s.params.ACLStore = aclstore.NewACLStore(memsimplekv.NewStore()) } s.server = httptest.NewUnstartedServer(nil) c.Defer(s.server.Close) s.params.Location = "http://" + s.server.Listener.Addr().String() + sublocation if s.params.Key == nil { var err error s.params.Key, err = bakery.GenerateKey() c.Assert(err, qt.IsNil) } s.Key = s.params.Key if s.params.PrivateAddr == "" { s.params.PrivateAddr = "localhost" } if s.params.AdminAgentPublicKey == nil { var err error s.adminAgentKey, err = bakery.GenerateKey() c.Assert(err, qt.IsNil) s.params.AdminAgentPublicKey = &s.adminAgentKey.Public } if s.params.Template == nil { s.params.Template = DefaultTemplate } var err error s.handler, err = identity.New(s.params, versions) c.Assert(err, qt.IsNil) c.Defer(s.handler.Close) s.server.Config.Handler = http.StripPrefix(sublocation, s.handler) s.server.Start() s.URL = s.server.URL ctx := context.Background() ctx, closeStore := s.params.Store.Context(ctx) c.Defer(closeStore) ctx, closeMeetingStore := s.params.MeetingStore.Context(ctx) c.Defer(closeMeetingStore) s.Ctx = ctx return s } // ThirdPartyInfo implements bakery.ThirdPartyLocator.ThirdPartyInfo // allowing the suite to be used as a bakery.ThirdPartyLocator. func (s *Server) ThirdPartyInfo(ctx context.Context, loc string) (bakery.ThirdPartyInfo, error) { if loc != s.URL { return bakery.ThirdPartyInfo{}, bakery.ErrNotFound } return bakery.ThirdPartyInfo{ PublicKey: s.params.Key.Public, Version: bakery.LatestVersion, }, nil } // Client is a convenience method that returns the result of // calling BakeryClient(i) func (s *Server) Client(i httpbakery.Interactor) *httpbakery.Client { return BakeryClient(i) } // BakeryClient creates a new httpbakery.Client which uses the given visitor as // its WebPageVisitor. If no Visitor is specified then NoVisit will be // used. func BakeryClient(i httpbakery.Interactor) *httpbakery.Client { cl := &httpbakery.Client{ Client: httpbakery.NewHTTPClient(), } if i != nil { cl.AddInteractor(i) } return cl } // AdminClient creates a new httpbakery.Client that is configured to log // in as an admin user. func (s *Server) AdminClient() *httpbakery.Client { client := &httpbakery.Client{ Client: httpbakery.NewHTTPClient(), Key: s.adminAgentKey, } agent.SetUpAuth(client, &agent.AuthInfo{ Key: s.adminAgentKey, Agents: []agent.Agent{{ URL: s.URL, Username: auth.AdminUsername, }}, }) return client } // AdminIdentityClient creates a new candidclient.Client that is configured to log // in as an admin user. func (s *Server) AdminIdentityClient(userID bool) *candidclient.Client { client, err := candidclient.New(candidclient.NewParams{ BaseURL: s.URL, Client: &httpbakery.Client{ Client: httpbakery.NewHTTPClient(), Key: s.adminAgentKey, }, AgentUsername: auth.AdminUsername, UseUserID: userID, }) if err != nil { panic(err) } return client } // CreateAgent creates a new agent user in the identity server's store // with the given name and groups. The agent's username and key are // returned. // // The agent will be owned by admin@candid. func (s *Server) CreateAgent(c *qt.C, username string, groups ...string) *bakery.KeyPair { key, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) name := strings.TrimSuffix(username, "@candid") if name == username { c.Fatalf("agent username must end in @candid") } err = s.params.Store.UpdateIdentity( context.Background(), &store.Identity{ ProviderID: store.MakeProviderIdentity("idm", name), Username: username, Groups: groups, PublicKeys: []bakery.PublicKey{ key.Public, }, Owner: auth.AdminProviderID, }, store.Update{ store.Username: store.Set, store.Groups: store.Set, store.PublicKeys: store.Set, store.Owner: store.Set, }, ) c.Assert(err, qt.IsNil) return key } // CreateUser creates a new user in the identity server's store with the // given name and groups. The user's username is returned. func (s *Server) CreateUser(c *qt.C, name string, groups ...string) string { err := s.params.Store.UpdateIdentity( context.Background(), &store.Identity{ ProviderID: store.MakeProviderIdentity("test", name), Username: name, Groups: groups, }, store.Update{ store.Username: store.Set, store.Groups: store.Set, }, ) c.Assert(err, qt.IsNil) return name } // IdentityClient creates a new agent with the given username // (which must end in @candid) and groups and then creates an // candidclient.Client // which authenticates using that agent. func (s *Server) IdentityClient(c *qt.C, username string, groups ...string) *candidclient.Client { key := s.CreateAgent(c, username, groups...) client, err := candidclient.New(candidclient.NewParams{ BaseURL: s.URL, Client: &httpbakery.Client{ Client: httpbakery.NewHTTPClient(), Key: key, }, AgentUsername: username, }) c.Assert(err, qt.IsNil) return client } // Do is a convenience function for performing HTTP requests against the // server. The server's URL will be prepended to the one specified in the // request and then the request will be performed using // http.DefaultClient. func (s *Server) Do(c *qt.C, req *http.Request) *http.Response { resp, err := http.DefaultClient.Do(s.reqUrl(c, req)) c.Assert(err, qt.IsNil) return resp } // Get is a convenience function for performing HTTP requests against the // server. The server's URL will be prepended to the one given and then // the GET will be performed using http.DefaultClient. func (s *Server) Get(c *qt.C, url string) *http.Response { req, err := http.NewRequest("GET", url, nil) c.Assert(err, qt.IsNil) return s.Do(c, req) } // RoundTrip is a convenience function for performing a single HTTP // requests against the server. The server's URL will be prepended to the // one specified in the request and then a single request will be // performed using http.DefaultTransport. func (s *Server) RoundTrip(c *qt.C, req *http.Request) *http.Response { resp, err := http.DefaultTransport.RoundTrip(s.reqUrl(c, req)) c.Assert(err, qt.IsNil) return resp } func (s *Server) reqUrl(c *qt.C, req *http.Request) *http.Request { u, err := url.Parse(s.URL) c.Assert(err, qt.IsNil) req.URL = u.ResolveReference(req.URL) return req } // NoVisit is a httpbakery.Visitor that returns an error without // attempting a visit. type NoVisit struct{} // VisitWebPage implements httpbakery.Visitor.VisitWebPage func (NoVisit) VisitWebPage(context.Context, *httpbakery.Client, map[string]*url.URL) error { return errgo.New("visit not supported") } golang-github-canonical-candid-1.12.3/internal/candidtest/store.go000066400000000000000000000056101457263123000251200ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package candidtest import ( "context" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" aclstore "github.com/juju/aclstore/v2" "github.com/juju/simplekv/memsimplekv" "github.com/canonical/candid/internal/identity" "github.com/canonical/candid/meeting" "github.com/canonical/candid/store" "github.com/canonical/candid/store/memstore" ) // Store implements a test fixture that contains memory-based // store implementations for use with tests. type Store struct { Store store.Store ProviderDataStore store.ProviderDataStore MeetingStore meeting.Store BakeryRootKeyStore bakery.RootKeyStore ACLStore aclstore.ACLStore } // NewStore returns a new Store that uses in-memory storage. func NewStore() *Store { return &Store{ Store: memstore.NewStore(), ProviderDataStore: memstore.NewProviderDataStore(), MeetingStore: memstore.NewMeetingStore(), BakeryRootKeyStore: bakery.NewMemRootKeyStore(), ACLStore: aclstore.NewACLStore(memsimplekv.NewStore()), } } // ServerParams returns parameters suitable for passing // to NewServer that will use s as its store. func (s *Store) ServerParams() identity.ServerParams { return identity.ServerParams{ Store: s.Store, ProviderDataStore: s.ProviderDataStore, MeetingStore: s.MeetingStore, RootKeyStore: s.BakeryRootKeyStore, ACLStore: s.ACLStore, } } // AssertUser asserts that the specified user is stored in the store. // It returns the stored identity. func (s *Store) AssertUser(c *qt.C, id *store.Identity) *store.Identity { id1 := store.Identity{ ProviderID: id.ProviderID, Username: id.Username, } err := s.Store.Identity(context.Background(), &id1) c.Assert(err, qt.IsNil) AssertEqualIdentity(c, &id1, id) return &id1 } // AssertEqualIdentity asserts that the two provided identites are // semantically equivilent. func AssertEqualIdentity(c *qt.C, obtained, expected *store.Identity) { if expected.ID == "" { obtained.ID = "" } normalizeInfoMap(obtained.ProviderInfo) normalizeInfoMap(obtained.ExtraInfo) normalizeInfoMap(expected.ProviderInfo) normalizeInfoMap(expected.ExtraInfo) opts := []cmp.Option{ cmpopts.EquateEmpty(), cmpopts.SortSlices(func(s, t string) bool { return s < t }), cmpopts.SortSlices(func(x, y bakery.PublicKey) bool { return string(x.Key[:]) < string(y.Key[:]) }), } msg := cmp.Diff(obtained, expected, opts...) if msg != "" { c.Fatalf("identities do not match: %s", msg) } } // normalizeInfoMap normalizes a providerInfo or extraInfo map by // removing any keys that have a 0 length value. func normalizeInfoMap(m map[string][]string) { for k, v := range m { if len(v) == 0 { delete(m, k) } } } golang-github-canonical-candid-1.12.3/internal/debug/000077500000000000000000000000001457263123000223775ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/internal/debug/debug.go000066400000000000000000000035221457263123000240160ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package debug import ( "context" "net/http" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/loggo" "github.com/juju/utils/v2/debugstatus" "gopkg.in/httprequest.v1" "github.com/canonical/candid/internal/identity" "github.com/canonical/candid/version" ) var logger = loggo.GetLogger("candid.internal.debug") var stdCheckers = []debugstatus.CheckerFunc{ debugstatus.ServerStartTime, } func NewAPIHandler(params identity.HandlerParams) ([]httprequest.Handler, error) { h := newDebugAPIHandler(params) handlers := []httprequest.Handler{{ Method: "GET", Path: "/debug/login", Handle: h.login, }, { Method: "POST", Path: "/debug/login", Handle: h.login, }} for _, hnd := range identity.ReqServer.Handlers(h.handler) { handlers = append(handlers, hnd) } return handlers, nil } func newDebugAPIHandler(params identity.HandlerParams) *debugAPIHandler { h := &debugAPIHandler{ key: params.Key, location: params.Location, teams: params.DebugTeams, } checkerFuncs := append(stdCheckers, params.DebugStatusCheckerFuncs...) h.hnd = debugstatus.Handler{ Check: func(ctx context.Context) map[string]debugstatus.CheckResult { // TODO (mhilton) re-instate meeting status checks. return debugstatus.Check(ctx, checkerFuncs...) }, Version: debugstatus.Version(version.VersionInfo), CheckPprofAllowed: h.checkLogin, CheckTraceAllowed: func(r *http.Request) (bool, error) { return false, h.checkLogin(r) }, } return h } type debugAPIHandler struct { key *bakery.KeyPair location string teams []string hnd debugstatus.Handler } func (h *debugAPIHandler) handler(p httprequest.Params) (*debugstatus.Handler, context.Context, error) { return &h.hnd, p.Context, nil } golang-github-canonical-candid-1.12.3/internal/debug/debug_test.go000066400000000000000000000061321457263123000250550ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package debug_test import ( "encoding/json" "net/http" "regexp" "testing" "time" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/juju/mgotest" "github.com/juju/qthttptest" "github.com/juju/utils/v2/debugstatus" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/internal/debug" "github.com/canonical/candid/internal/identity" "github.com/canonical/candid/store/mgostore" buildver "github.com/canonical/candid/version" ) const ( version = "debug" ) func TestDebug(t *testing.T) { qtsuite.Run(qt.New(t), &debugSuite{}) } type debugSuite struct { srv *candidtest.Server } func (s *debugSuite) Init(c *qt.C) { s.srv = newFixture(c).srv } func (s *debugSuite) patchStartTime(c *qt.C) time.Time { startTime := time.Now() c.Patch(&debugstatus.StartTime, startTime) return startTime } func (s *debugSuite) TestServeDebugStatus(c *qt.C) { startTime := s.patchStartTime(c) expectNames := map[string]string{ "server_started": "Server started", "mongo_collections": "MongoDB collections", "meeting_count": "count of meeting collection", } expectValues := map[string]string{ "server_started": regexp.QuoteMeta(startTime.String()), "mongo_collections": "All required collections exist", "meeting_count": "0", } qthttptest.AssertJSONCall(c, qthttptest.JSONCallParams{ URL: s.srv.URL + "/debug/status", ExpectBody: qthttptest.BodyAsserter(func(c *qt.C, body json.RawMessage) { var result map[string]debugstatus.CheckResult err := json.Unmarshal(body, &result) c.Assert(err, qt.IsNil) c.Assert(result, qt.HasLen, len(expectNames)) for k, v := range result { c.Assert(v.Name, qt.Equals, expectNames[k], qt.Commentf("%s: incorrect name", k)) c.Assert(v.Value, qt.Matches, expectValues[k], qt.Commentf("%s: incorrect value", k)) } }), }) } func (s *debugSuite) TestServeDebugInfo(c *qt.C) { qthttptest.AssertJSONCall(c, qthttptest.JSONCallParams{ URL: s.srv.URL + "/debug/info", ExpectStatus: http.StatusOK, ExpectBody: buildver.VersionInfo, }) } type fixture struct { srv *candidtest.Server } func newFixture(c *qt.C) *fixture { db, err := mgotest.New() if errgo.Cause(err) == mgotest.ErrDisabled { c.Skip("mgotest disabled") } c.Assert(err, qt.IsNil) // mgotest sets the SocketTimout to 1s. Restore it back to the // default value. db.Session.SetSocketTimeout(time.Minute) backend, err := mgostore.NewBackend(db.Database) if err != nil { db.Close() c.Fatal(err) } c.Assert(err, qt.IsNil) c.Defer(backend.Close) sp := identity.ServerParams{ MeetingStore: backend.MeetingStore(), RootKeyStore: backend.BakeryRootKeyStore(), Store: backend.Store(), DebugStatusCheckerFuncs: backend.DebugStatusCheckerFuncs(), DebugTeams: []string{"debuggers"}, } srv := candidtest.NewServer(c, sp, map[string]identity.NewAPIHandlerFunc{ version: debug.NewAPIHandler, }) return &fixture{ srv: srv, } } golang-github-canonical-candid-1.12.3/internal/debug/export_test.go000066400000000000000000000012411457263123000253040ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package debug import "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" type ( DebugAPIHandler *debugAPIHandler Cookie cookie ) var ( New = newDebugAPIHandler ) // DecodeCookie is a wrapper around decodeCookie that can be used for // testing. func DecodeCookie(k *bakery.KeyPair, s string) (*Cookie, error) { c, err := decodeCookie(k, s) return (*Cookie)(c), err } // EncodeCookie is a wrapper around encodeCookie that can be used for // testing. func EncodeCookie(k *bakery.KeyPair, c *Cookie) (string, error) { return encodeCookie(k, (*cookie)(c)) } golang-github-canonical-candid-1.12.3/internal/debug/login.go000066400000000000000000000117661457263123000240510ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package debug import ( "crypto/rand" "encoding/base64" "encoding/json" "fmt" "net/http" "net/url" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/usso" "github.com/juju/usso/openid" "github.com/julienschmidt/httprouter" "golang.org/x/crypto/nacl/box" "gopkg.in/errgo.v1" "github.com/canonical/candid/internal/identity" "github.com/canonical/candid/params" ) const cookieName = "debug-login" // loginRequiredError is an error that indicates that a login request // should be attempted. type loginRequiredError struct { redirectURL string } func (*loginRequiredError) ErrorCode() params.ErrorCode { return identity.ErrLoginRequired } // Error implements error.Error. func (err *loginRequiredError) Error() string { return fmt.Sprintf("login required to %q", err.redirectURL) } // SetHeader implements httprequest.HeaderSetter. func (err *loginRequiredError) SetHeader(h http.Header) { h.Set("Location", err.redirectURL) } // cookie contains the data stored in the debug login cookie. type cookie struct { // ExpireTime contains the time after which the cookie is // invalid. ExpireTime time.Time // ID contains the Ubuntu SSO ID of the user. ID string // Teams contains the subset of params.DebugTeams of which the // user is a member. Teams []string } func encodeCookie(k *bakery.KeyPair, c *cookie) (string, error) { data, err := json.Marshal(c) if err != nil { return "", errgo.Mask(err) } var nonce [24]byte _, err = rand.Read(nonce[:]) if err != nil { return "", errgo.Mask(err) } edata := nonce[:] edata = box.Seal(edata, data, &nonce, (*[bakery.KeyLen]byte)(&k.Public.Key), (*[bakery.KeyLen]byte)(&k.Private.Key)) return base64.StdEncoding.EncodeToString(edata), nil } func decodeCookie(k *bakery.KeyPair, v string) (*cookie, error) { edata, err := base64.StdEncoding.DecodeString(v) if err != nil { return nil, errgo.Notef(err, "cannot decode cookie") } var nonce [24]byte n := copy(nonce[:], edata) edata = edata[n:] data, ok := box.Open(nil, edata, &nonce, (*[bakery.KeyLen]byte)(&k.Public.Key), (*[bakery.KeyLen]byte)(&k.Private.Key)) if !ok { return nil, errgo.New("cannot decrypt cookie") } var cookie cookie if err := json.Unmarshal(data, &cookie); err != nil { return nil, errgo.Notef(err, "cannot unmarshal cookie") } return &cookie, nil } // checkLogin checks that the given request contains a valid login cookie // for accessing /debug endpoints. If there is any sort of error decoding // the cookie the returned error will have a cause of type // *loginRequiredError. func (h *debugAPIHandler) checkLogin(r *http.Request) error { c, err := r.Cookie(cookieName) if err != nil { return errgo.WithCausef(err, h.loginRequired(r), "no cookie") } cookie, err := decodeCookie(h.key, c.Value) if err != nil { return errgo.WithCausef(nil, h.loginRequired(r), "%s", err.Error()) } if cookie.ExpireTime.Before(time.Now()) { return errgo.WithCausef(nil, h.loginRequired(r), "cookie expired") } for _, t1 := range cookie.Teams { for _, t2 := range h.teams { if t1 == t2 { return nil } } } return errgo.WithCausef(nil, h.loginRequired(r), "no suitable team membership") } // ussoClient is the client for the Ubuntu SSO server. var ussoClient = openid.NewClient(usso.ProductionUbuntuSSOServer, nil, nil) // loginRequired creates a new loginRequiredError for the given request. func (h *debugAPIHandler) loginRequired(r *http.Request) *loginRequiredError { return &loginRequiredError{ redirectURL: ussoClient.RedirectURL(&openid.Request{ ReturnTo: h.location + "/debug/login?return_to=" + url.QueryEscape(h.location+r.URL.String()), Realm: h.location + "/debug", Teams: h.teams, }), } } // login handles callbacks from an Ubuntu SSO login attempt. func (h *debugAPIHandler) login(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { url := h.location + r.URL.String() resp, err := ussoClient.Verify(url) if err != nil { w.WriteHeader(http.StatusUnauthorized) fmt.Fprintf(w, "unauthorized: %s\n", err) return } for _, t1 := range resp.Teams { for _, t2 := range h.teams { if t1 == t2 { h.loginSuccess(w, r, resp) return } } } w.WriteHeader(http.StatusUnauthorized) fmt.Fprintf(w, "unauthorized: access denied for %s", resp.ID) } // loginSuccess completes a login when it has been deemed successful. func (h *debugAPIHandler) loginSuccess(w http.ResponseWriter, r *http.Request, resp *openid.Response) { c := &cookie{ ExpireTime: time.Now().Add(1 * time.Hour), ID: resp.ID, Teams: resp.Teams, } value, err := encodeCookie(h.key, c) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "cannot create cookie: %s", err) return } http.SetCookie(w, &http.Cookie{ Name: cookieName, Value: value, Path: "/debug", Expires: c.ExpireTime, }) r.ParseForm() w.Header().Set("Location", r.Form.Get("return_to")) w.WriteHeader(http.StatusSeeOther) } golang-github-canonical-candid-1.12.3/internal/debug/login_test.go000066400000000000000000000116461457263123000251050ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package debug_test import ( "crypto/rand" "encoding/base64" "encoding/json" "net/http" "net/url" "testing" "time" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/qthttptest" "golang.org/x/crypto/nacl/box" "gopkg.in/errgo.v1" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/internal/debug" ) func TestLogin(t *testing.T) { qtsuite.Run(qt.New(t), &loginSuite{}) } type loginSuite struct { srv *candidtest.Server } func (s *loginSuite) Init(c *qt.C) { s.srv = newFixture(c).srv } func (s *loginSuite) TestCookieEncodeDecode(c *qt.C) { c1 := &debug.Cookie{ ExpireTime: time.Now(), ID: "https://example.com/ID", Teams: []string{"t1", "t2"}, } v, err := debug.EncodeCookie(s.srv.Key, c1) c.Assert(err, qt.IsNil) c2, err := debug.DecodeCookie(s.srv.Key, v) c.Assert(err, qt.IsNil) c.Assert(c1.ExpireTime.Equal(c1.ExpireTime), qt.Equals, true, qt.Commentf("expire times not equal expecting: %s, obtained: %s", c1.ExpireTime, c2.ExpireTime)) c1.ExpireTime = time.Time{} c2.ExpireTime = time.Time{} c.Assert(c2, qt.DeepEquals, c1) } var testCheckLogin = []struct { about string cookieValue func(key *bakery.KeyPair) (string, error) expectLoginRequest bool }{{ about: "good cookie", cookieValue: cookieEncode(debug.Cookie{ ExpireTime: time.Now().Add(1 * time.Hour), Teams: []string{"debuggers"}, }), }, { about: "no cookie", expectLoginRequest: true, }, { about: "too old", cookieValue: cookieEncode(debug.Cookie{ ExpireTime: time.Now().Add(-1 * time.Minute), Teams: []string{"debuggers"}, }), expectLoginRequest: true, }, { about: "wrong teams", cookieValue: cookieEncode(debug.Cookie{ ExpireTime: time.Now().Add(1 * time.Hour), Teams: []string{"not-debuggers"}, }), expectLoginRequest: true, }, { about: "bad base64", cookieValue: func(*bakery.KeyPair) (string, error) { return "A", nil }, expectLoginRequest: true, }, { about: "wrong key", cookieValue: func(*bakery.KeyPair) (string, error) { k2, err := bakery.GenerateKey() if err != nil { return "", err } return cookieEncode(debug.Cookie{ ExpireTime: time.Now().Add(1 * time.Hour), Teams: []string{"debuggers"}, })(k2) }, expectLoginRequest: true, }, { about: "wrong signing key", cookieValue: func(key *bakery.KeyPair) (string, error) { k2, err := bakery.GenerateKey() if err != nil { return "", err } k3 := &bakery.KeyPair{ Public: key.Public, Private: k2.Private, } return cookieEncode(debug.Cookie{ ExpireTime: time.Now().Add(1 * time.Hour), Teams: []string{"debuggers"}, })(k3) }, expectLoginRequest: true, }, { about: "bad json", cookieValue: func(key *bakery.KeyPair) (string, error) { data := []byte("{") var nonce [24]byte _, err := rand.Read(nonce[:]) if err != nil { return "", err } edata := nonce[:] edata = box.Seal(edata, data, &nonce, (*[bakery.KeyLen]byte)(&key.Public.Key), (*[bakery.KeyLen]byte)(&key.Private.Key)) return base64.StdEncoding.EncodeToString(edata), nil }, expectLoginRequest: true, }} func (s *loginSuite) TestCheckLogin(c *qt.C) { for i, test := range testCheckLogin { c.Logf("%d. %s", i, test.about) var cookies []*http.Cookie if test.cookieValue != nil { value, err := test.cookieValue(s.srv.Key) c.Assert(err, qt.IsNil) cookies = append(cookies, &http.Cookie{ Name: "debug-login", Value: value, }) } resp := qthttptest.Do(c, qthttptest.DoRequestParams{ URL: s.srv.URL + "/debug/pprof/", Do: doNoRedirect, Cookies: cookies, }) defer resp.Body.Close() if !test.expectLoginRequest { c.Assert(resp.StatusCode, qt.Equals, http.StatusOK) continue } c.Assert(resp.StatusCode, qt.Equals, http.StatusFound) c.Assert(resp.Header.Get("Location"), qt.Not(qt.Equals), "") } } func cookieEncode(v interface{}) func(*bakery.KeyPair) (string, error) { return func(key *bakery.KeyPair) (string, error) { data, err := json.Marshal(v) if err != nil { return "", err } var nonce [24]byte _, err = rand.Read(nonce[:]) if err != nil { return "", err } edata := nonce[:] edata = box.Seal(edata, data, &nonce, (*[bakery.KeyLen]byte)(&key.Public.Key), (*[bakery.KeyLen]byte)(&key.Private.Key)) return base64.StdEncoding.EncodeToString(edata), nil } } func doNoRedirect(req *http.Request) (*http.Response, error) { resp, err := noRedirectClient.Do(req) if err == nil { return resp, nil } if uerr, ok := err.(*url.Error); ok { err := uerr.Err if errgo.Cause(err) == errStopRedirect { return resp, nil } } return resp, err } var errStopRedirect = errgo.New("no redirects") var noRedirectClient = &http.Client{ CheckRedirect: func(*http.Request, []*http.Request) error { return errStopRedirect }, } golang-github-canonical-candid-1.12.3/internal/discharger/000077500000000000000000000000001457263123000234245ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/internal/discharger/agent.go000066400000000000000000000153461457263123000250620ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package discharger import ( "context" "net/http" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" errgo "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/canonical/candid/candidclient" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) const ( // agentMacaroonDuration is the length of time for which an agent // identity macaroon is valid. This is shorter than for users as // an agent can authenticate without interaction. agentMacaroonDuration = 30 * time.Minute // agentLoginMacaroonDuration is the lifetime of the intermediate // macaroon used in the agent login process. agentLoginMacaroonDuration = 10 * time.Second ) func loginOp(user string) bakery.Op { return bakery.Op{ Entity: "agent-" + user, Action: "login", } } // agentLoginRequest is the expected GET request to the agent-login // endpoint. Note: this is compatible with the parameters used for the // agent login request in the httpbakery/agent package. type agentLoginRequest struct { httprequest.Route `httprequest:"GET /login/agent"` DischargeID string `httprequest:"did,form"` Username string `httprequest:"username,form"` PublicKey *bakery.PublicKey `httprequest:"public-key,form"` } type agentMacaroonResponse struct { Macaroon *bakery.Macaroon `json:"macaroon"` } // agentURL returns the URL path for the agent-login endpoint for the // candid service at the given location. func agentURL(location string, dischargeID string) string { p := location + "/login/agent" if dischargeID != "" { p += "?did=" + dischargeID } return p } // AgentLogin is the endpoint used to acquire an agent macaroon // as part of a discharge request. func (h *handler) AgentLogin(p httprequest.Params, req *agentLoginRequest) (*agentMacaroonResponse, error) { if req.Username == "" { return nil, errgo.WithCausef(nil, params.ErrBadRequest, "username not specified") } if req.PublicKey == nil { return nil, errgo.WithCausef(nil, params.ErrBadRequest, "public-key not specified") } m, err := h.agentMacaroon(p.Context, httpbakery.RequestVersion(p.Request), identchecker.LoginOp, req.Username, req.PublicKey) if err != nil { return nil, errgo.Mask(err) } return &agentMacaroonResponse{Macaroon: m}, nil } // agentMacaroon creates a new macaroon containing a local third-party // caveat addressed to the specified agent. func (h *handler) agentMacaroon(ctx context.Context, vers bakery.Version, op bakery.Op, user string, key *bakery.PublicKey) (*bakery.Macaroon, error) { m, err := h.params.Oven.NewMacaroon( ctx, vers, []checkers.Caveat{ checkers.TimeBeforeCaveat(time.Now().Add(agentLoginMacaroonDuration)), candidclient.UserDeclaration(user), bakery.LocalThirdPartyCaveat(key, vers), auth.UserHasPublicKeyCaveat(params.Username(user), key), }, op, ) return m, errgo.Mask(err) } // legacyAgentLoginRequest is the expected GET request to the agent-login // endpoint. type legacyAgentLoginRequest struct { httprequest.Route `httprequest:"GET /login/legacy-agent"` DischargeID string `httprequest:"did,form"` } // LegacyAgentLogin is the endpoint used when performing agent login // using the legacy agent-login cookie based protocols. func (h *handler) LegacyAgentLogin(p httprequest.Params, req *legacyAgentLoginRequest) (interface{}, error) { user, key, err := agent.LoginCookie(p.Request) if err != nil { if errgo.Cause(err) == agent.ErrNoAgentLoginCookie { return nil, errgo.WithCausef(err, params.ErrBadRequest, "") } return nil, errgo.Mask(err) } resp, err := h.legacyAgentLogin(p.Context, p.Request, req.DischargeID, user, key) if err != nil { return nil, errgo.Mask(err, errgo.Any) } return resp, nil } // legacyAgentLoginPostRequest is the expected request to the agent-login // endpoint when using the POST protocol. type legacyAgentLoginPostRequest struct { httprequest.Route `httprequest:"POST /login/legacy-agent"` DischargeID string `httprequest:"did,form"` AgentLogin params.AgentLogin `httprequest:",body"` } // LegacyAgentLoginPost is the endpoint used when performing an agent login // using the POST protocol. func (h *handler) LegacyAgentLoginPost(p httprequest.Params, req *legacyAgentLoginPostRequest) (*agent.LegacyAgentResponse, error) { resp, err := h.legacyAgentLogin(p.Context, p.Request, req.DischargeID, string(req.AgentLogin.Username), req.AgentLogin.PublicKey) if err != nil { return nil, errgo.Mask(err, errgo.Any) } return resp, nil } // legacyAgentLogin handles the common parts of the legacy agent login protocols. func (h *handler) legacyAgentLogin(ctx context.Context, req *http.Request, dischargeID string, user string, key *bakery.PublicKey) (*agent.LegacyAgentResponse, error) { loginOp := loginOp(user) vers := httpbakery.RequestVersion(req) ctx = httpbakery.ContextWithRequest(ctx, req) ctx = auth.ContextWithDischargeID(ctx, dischargeID) _, err := h.params.Authorizer.Auth(ctx, httpbakery.RequestMacaroons(req), loginOp) if err == nil { id := store.Identity{ Username: user, } if err := h.params.Store.Identity(ctx, &id); err != nil { // This will always be unexpected as if the verification succeeded // the identity must exist. return nil, errgo.Mask(err) } h.params.place.Done(ctx, dischargeID, &loginInfo{ ProviderID: id.ProviderID, }) return &agent.LegacyAgentResponse{ AgentLogin: true, }, nil } // TODO fail harder if the error isn't because of a verification error? // Verification has failed. The bakery checker will want us to // discharge a macaroon to prove identity, but we're already // part of the discharge process so we can't do that here. // Instead, mint a very short term macaroon containing // the local third party caveat that will allow access if discharged. m, err := h.agentMacaroon(ctx, vers, loginOp, user, key) if err != nil { return nil, errgo.Notef(err, "cannot create macaroon") } return nil, httpbakery.NewDischargeRequiredError(httpbakery.DischargeRequiredErrorParams{ Macaroon: m, Request: req, CookieNameSuffix: "agent-login", }) } // legacyAgentURL returns the URL path for the legacy agent login endpoint // for the candid service at the given location. func legacyAgentURL(location string, dischargeID string) string { p := location + "/login/legacy-agent" if dischargeID != "" { p += "?did=" + dischargeID } return p } golang-github-canonical-candid-1.12.3/internal/discharger/agent_test.go000066400000000000000000000164461457263123000261230ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package discharger_test import ( "bytes" "context" "encoding/base64" "encoding/json" "io/ioutil" "net/http" "net/url" "strings" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" errgo "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/internal/discharger" "github.com/canonical/candid/internal/identity" "github.com/canonical/candid/params" ) type agentSuite struct { srv *candidtest.Server store *candidtest.Store dischargeCreator *candidtest.DischargeCreator } func (s *agentSuite) Init(c *qt.C) { s.srv = candidtest.NewMemServer(c, map[string]identity.NewAPIHandlerFunc{ "discharger": discharger.NewAPIHandler, }) s.dischargeCreator = candidtest.NewDischargeCreator(s.srv) } func (s *agentSuite) TestHTTPBakeryAgentDischarge(c *qt.C) { key := s.srv.CreateAgent(c, "bob@candid") client := s.srv.Client(nil) client.Key = key err := agent.SetUpAuth(client, &agent.AuthInfo{ Key: client.Key, Agents: []agent.Agent{{ URL: s.srv.URL, Username: "bob@candid", }}, }) c.Assert(err, qt.IsNil) ms, err := s.dischargeCreator.Discharge(c, "is-authenticated-user", client) c.Assert(err, qt.IsNil) _, err = s.dischargeCreator.Bakery.Checker.Auth(ms).Allow(context.Background(), identchecker.LoginOp) c.Assert(err, qt.IsNil) } func (s *agentSuite) TestGetAgentDischargeNoCookie(c *qt.C) { client := &httprequest.Client{ BaseURL: s.srv.URL, } err := client.Get(context.Background(), "/login/legacy-agent", nil) c.Assert(err, qt.ErrorMatches, `Get http://.*/login/legacy-agent: no agent-login cookie found`) } func (s *agentSuite) TestLegacyAgentDischarge(c *qt.C) { key := s.srv.CreateAgent(c, "bob@candid") client := s.srv.Client(nil) client.Key = key // Set up the transport so that it mutates /discharge responses // to delete the interaction methods so the client exercises // the legacy protocol instead of the current one. client.Transport = fakeLegacyServerTransport{client.Transport} err := agent.SetUpAuth(client, &agent.AuthInfo{ Key: client.Key, Agents: []agent.Agent{{ URL: s.srv.URL, Username: "bob@candid", }}, }) c.Assert(err, qt.IsNil) ms, err := s.dischargeCreator.Discharge(c, "is-authenticated-user", client) c.Assert(err, qt.IsNil) _, err = s.dischargeCreator.Bakery.Checker.Auth(ms).Allow(context.Background(), identchecker.LoginOp) c.Assert(err, qt.IsNil) } func (s *agentSuite) TestLegacyCookieAgentDischarge(c *qt.C) { // legacy agent protocol with cookie: // Agent Login Service // | | // | GET visitURL with agent cookie | // |----------------------------------->| // | | // | Macaroon with local third-party | // | caveat | // |<-----------------------------------| // | | // | GET visitURL with agent cookie & | // | discharged macaroon | // |----------------------------------->| // | | // | Agent login response | // |<-----------------------------------| // | | // Note that we don't need the agent interactor in this // scenario. key := s.srv.CreateAgent(c, "bob@candid") var visit func(u *url.URL) error client := s.srv.Client(httpbakery.WebBrowserInteractor{ OpenWebBrowser: func(u *url.URL) error { return visit(u) }, }) client.Key = key // Set up the transport so that it mutates /discharge responses // to delete the interaction methods so the client exercises // the legacy protocol instead of the current one. client.Transport = fakeLegacyServerTransport{client.Transport} visitCalled := false visit = func(u *url.URL) error { req, err := http.NewRequest("GET", u.String(), nil) c.Assert(err, qt.IsNil) resp, err := client.Do(req) c.Assert(err, qt.IsNil) resp.Body.Close() visitCalled = true return nil } // Set up a cookie so that the /discharge endpoint will see // it and respond with a self-dischargable interaction-required // error. s.setAgentCookie(client.Jar, "bob@candid", &key.Public) ms, err := s.dischargeCreator.Discharge(c, "is-authenticated-user", client) c.Assert(err, qt.IsNil) _, err = s.dischargeCreator.Bakery.Checker.Auth(ms).Allow(context.Background(), identchecker.LoginOp) c.Assert(err, qt.IsNil) c.Assert(visitCalled, qt.Equals, true) } func (s *agentSuite) setAgentCookie(jar http.CookieJar, username string, pk *bakery.PublicKey) { u, err := url.Parse(s.srv.URL) if err != nil { panic(err) } al := agentLogin{ Username: string(username), PublicKey: pk, } buf, err := json.Marshal(al) if err != nil { panic(err) } jar.SetCookies(u, []*http.Cookie{{ Name: "agent-login", Value: base64.URLEncoding.EncodeToString(buf), }}) } type agentLoginRequest struct { httprequest.Route `httprequest:"POST"` params.AgentLogin `httprequest:",body"` } type legacyAgentVisitor struct { username params.Username pk *bakery.PublicKey client *httpbakery.Client } // agentLogin defines the structure of an agent login cookie. type agentLogin struct { Username string `json:"username"` PublicKey *bakery.PublicKey `json:"public_key"` } func (v *legacyAgentVisitor) OpenWebBrowserCookie(u *url.URL) error { al := agentLogin{ Username: string(v.username), PublicKey: v.pk, } buf, err := json.Marshal(al) if err != nil { return errgo.Mask(err) } cookie := &http.Cookie{ Name: "agent-login", Value: base64.URLEncoding.EncodeToString(buf), } req, err := http.NewRequest("GET", u.String(), nil) if err != nil { return errgo.Mask(err) } req.AddCookie(cookie) cl := &httprequest.Client{ Doer: v.client, } if err := cl.Do(context.Background(), req, nil); err != nil { return errgo.Mask(err) } return nil } // fakeLegacyServerTransport implements an HTTP transport // that rewrites discharge error responses to remove the new // InteractionMethods field so that the bakery client will // recognise it as a legacy response and proceed accordingly. type fakeLegacyServerTransport struct { t http.RoundTripper } var unmarshalBakeryError = httprequest.ErrorUnmarshaler(&httpbakery.Error{}) func (t fakeLegacyServerTransport) RoundTrip(req *http.Request) (*http.Response, error) { if t.t == nil { t.t = http.DefaultTransport } resp, err := t.t.RoundTrip(req) if !strings.HasSuffix(req.URL.Path, "/discharge") || err != nil || resp.StatusCode == http.StatusOK { return resp, err } err = unmarshalBakeryError(resp) berr, ok := err.(*httpbakery.Error) if !ok { panic("non-bakery error returned from discharge endpoint") } if berr.Info != nil { berr.Info.InteractionMethods = nil } resp.Body.Close() bodyData, err := json.Marshal(berr) if err != nil { panic("cannot re-marshal bakery error") } resp.Body = ioutil.NopCloser(bytes.NewReader(bodyData)) return resp, nil } golang-github-canonical-candid-1.12.3/internal/discharger/api.go000066400000000000000000000136311457263123000245300ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package discharger serves all of the endpoints related to discharging // macaroon and logging in. package discharger import ( "context" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/loggo" "golang.org/x/net/trace" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil/secret" "github.com/canonical/candid/internal/auth/httpauth" "github.com/canonical/candid/internal/discharger/internal" "github.com/canonical/candid/internal/identity" "github.com/canonical/candid/internal/monitoring" "github.com/canonical/candid/params" ) var logger = loggo.GetLogger("candid.internal.discharger") // NewAPIHandler is an identity.NewAPIHandlerFunc. func NewAPIHandler(params identity.HandlerParams) ([]httprequest.Handler, error) { reqAuth := httpauth.New(params.Oven, params.Authorizer, params.APIMacaroonTimeout) place := &place{params.MeetingPlace} dt := &dischargeTokenCreator{ params: params, } pidks, err := params.ProviderDataStore.KeyValueStore(context.Background(), "_provider_identity") if err != nil { return nil, errgo.Mask(err) } idstore := internal.NewIdentityStore(pidks, params.Store) vc := &visitCompleter{ params: params, identityStore: idstore, place: place, } codec := secret.NewCodec(params.Key) err = initIDPs(context.Background(), initIDPParams{ HandlerParams: params, Codec: codec, DischargeTokenCreator: dt, VisitCompleter: vc, }) if err != nil { return nil, errgo.Mask(err) } if params.MFAAuthenticator != nil { params.MFAAuthenticator.Init(idp.InitParams{ Store: params.Store, Oven: params.Oven, Codec: codec, Location: params.Location, URLPrefix: params.Location + "/login/mfa", VisitCompleter: vc, Template: params.Template, SkipLocationForCookiePaths: params.SkipLocationForCookiePaths, }) vc.mfaAuthenticator = params.MFAAuthenticator } checker := &thirdPartyCaveatChecker{ params: params, place: place, reqAuth: reqAuth, } handlers := identity.ReqServer.Handlers(handlerCreator(handlerParams{ HandlerParams: params, checker: checker, dischargeTokenCreator: dt, identityStore: idstore, visitCompleter: vc, place: place, reqAuth: reqAuth, codec: codec, })) d := httpbakery.NewDischarger(httpbakery.DischargerParams{ CheckerP: checker, Key: params.Key, ErrorToResponse: identity.ReqServer.ErrorMapper, }) for _, h := range d.Handlers() { handlers = append(handlers, h) // also add the discharger endpoint at the legacy location. handlers = append(handlers, httprequest.Handler{ Method: h.Method, Path: "/v1/discharger" + h.Path, Handle: h.Handle, }) } handlers = append(handlers, idpHandlers(params)...) handlers = append(handlers, mfaHandlers(params)...) return handlers, nil } type handlerParams struct { identity.HandlerParams checker *thirdPartyCaveatChecker dischargeTokenCreator *dischargeTokenCreator identityStore *internal.IdentityStore visitCompleter *visitCompleter place *place reqAuth *httpauth.Authorizer codec *secret.Codec } // handlerCreator returns a function that creates new instances of the discharger API handler for a request. func handlerCreator(hParams handlerParams) func(p httprequest.Params, arg interface{}) (*handler, context.Context, error) { return func(p httprequest.Params, arg interface{}) (*handler, context.Context, error) { t := trace.New(p.Request.URL.Path, p.PathPattern) ctx := trace.NewContext(p.Context, t) ctx, close1 := hParams.Store.Context(ctx) ctx, close2 := hParams.MeetingStore.Context(ctx) hnd := &handler{ params: hParams, trace: t, monReq: monitoring.NewRequest(&p), close: func() { close2() close1() }, } op := opForRequest(arg) logger.Debugf("opForRequest %#v -> %#v", arg, op) if op.Entity == "" { hnd.Close() return nil, nil, params.ErrUnauthorized } _, err := hParams.reqAuth.Auth(ctx, p.Request, op) if err != nil { hnd.Close() return nil, nil, errgo.Mask(err, errgo.Any) } return hnd, ctx, nil } } // A handler handles a request to a discharge related endpoint. type handler struct { params handlerParams monReq monitoring.Request trace trace.Trace close func() } // Close implements io.Closer. httprequest will automatically call this // once a request is complete. func (h *handler) Close() error { h.close() h.trace.Finish() h.monReq.ObserveMetric() return nil } func idpHandlers(params identity.HandlerParams) []httprequest.Handler { var handlers []httprequest.Handler for _, idp := range params.IdentityProviders { idp := idp path := "/login/" + idp.Name() + "/*path" hfunc := newIDPHandler(params, idp) handlers = append(handlers, httprequest.Handler{ Method: "GET", Path: path, Handle: hfunc, }, httprequest.Handler{ Method: "POST", Path: path, Handle: hfunc, }, httprequest.Handler{ Method: "PUT", Path: path, Handle: hfunc, }, ) } return handlers } func mfaHandlers(params identity.HandlerParams) []httprequest.Handler { if params.MFAAuthenticator == nil { return nil } handlers := []httprequest.Handler{} path := "/login/mfa/*path" hfunc := newMFAHandler(params, params.MFAAuthenticator) handlers = append(handlers, httprequest.Handler{ Method: "GET", Path: path, Handle: hfunc, }, httprequest.Handler{ Method: "POST", Path: path, Handle: hfunc, }, httprequest.Handler{ Method: "DELETE", Path: path, Handle: hfunc, }, ) return handlers } golang-github-canonical-candid-1.12.3/internal/discharger/auth.go000066400000000000000000000010231457263123000247100ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package discharger import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/canonical/candid/internal/auth" ) // opForRequest returns the operation that will be performed // by the API handler method which takes the given argument r. func opForRequest(_ interface{}) bakery.Op { // All of the endpoints are part of the login action and can be // accessed by anyone. return auth.GlobalOp(auth.ActionLogin) } golang-github-canonical-candid-1.12.3/internal/discharger/discharge.go000066400000000000000000000232421457263123000257070ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package discharger import ( "context" "crypto/rand" "encoding" "fmt" "net/http" "net/url" "strings" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" "github.com/juju/names/v4" "golang.org/x/net/trace" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "gopkg.in/macaroon.v2" "github.com/canonical/candid/candidclient" "github.com/canonical/candid/candidclient/redirect" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/internal/auth/httpauth" "github.com/canonical/candid/internal/identity" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) // thirdPartyCaveatChecker implements an // httpbakery.ThirdPartyCaveatChecker for the identity service. type thirdPartyCaveatChecker struct { params identity.HandlerParams reqAuth *httpauth.Authorizer checker *bakery.Checker place *place } // CheckThirdPartyCaveat implements httpbakery.ThirdPartyCaveatChecker. // It acquires a handler before checking the caveat, so that we have a // database connection for the purpose. func (c *thirdPartyCaveatChecker) CheckThirdPartyCaveat(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { t := trace.New(p.Request.URL.Path, "") defer t.Finish() return c.checkThirdPartyCaveat(trace.NewContext(ctx, t), p) } // checkThirdPartyCaveat checks the given caveat. This function is called // by the httpbakery discharge logic. See httpbakery.DischargeHandler // for futher details. // // This is implemented as a separate method so that it can be called from // WaitLegacy without nesting the trace context. func (c *thirdPartyCaveatChecker) checkThirdPartyCaveat(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { domain := "" if c, err := p.Request.Cookie("domain"); err == nil && names.IsValidUserDomain(c.Value) { domain = c.Value } cond, args, err := checkers.ParseCaveat(string(p.Caveat.Condition)) if err != nil { return nil, errgo.WithCausef(err, params.ErrBadRequest, "cannot parse caveat %q", p.Caveat.Condition) } forceLegacy := false if strings.HasPrefix(cond, "<") { cond = cond[1:] forceLegacy = true } var op bakery.Op switch cond { case "is-authenticated-user", "is-authenticated-userid": op = auth.GlobalOp(auth.ActionDischarge) if len(args) == 0 { break } if args[0] != '@' { return nil, checkers.ErrCaveatNotRecognized } if !names.IsValidUserDomain(args[1:]) { return nil, errgo.WithCausef(err, params.ErrBadRequest, "invalid domain %q", args[1:]) } domain = args[1:] ctx = auth.ContextWithRequiredDomain(ctx, domain) case "is-member-of": op = auth.GroupsDischargeOp(strings.Fields(args)) default: return nil, checkers.ErrCaveatNotRecognized } var mss []macaroon.Slice if user := p.Request.Form.Get("discharge-for-user"); user != "" { _, err = c.reqAuth.Auth(ctx, p.Request, auth.GlobalOp(auth.ActionDischargeFor)) if err != nil { return nil, errgo.Mask(err, errgo.Is(params.ErrUnauthorized), isDischargeRequiredError) } ctx = auth.ContextWithUsername(ctx, user) } else if p.Token != nil { tokenMacaroons, err := macaroonsFromDischargeToken(ctx, p.Token) if err != nil { return nil, errgo.Mask(err) } mss = []macaroon.Slice{tokenMacaroons} } else { // If no discharge token has been provided, include macaroons // from the request too, to enable clients to re-use previous discharge tokens that // have been returned as cookies. mss = httpbakery.RequestMacaroons(p.Request) } authInfo, err := c.params.Authorizer.Auth(ctx, mss, op) if _, ok := errgo.Cause(err).(*bakery.DischargeRequiredError); ok { return nil, c.interactionRequiredError(ctx, interactionRequiredParams{ why: err, forceLegacy: forceLegacy, req: p.Request, info: &dischargeRequestInfo{ Caveat: p.Caveat.Caveat, CaveatId: p.Caveat.Id, Condition: string(p.Caveat.Condition), Origin: p.Request.Header.Get("Origin"), }, domain: domain, }) } if err != nil { // TODO return appropriate error code when permission denied. return nil, errgo.Mask(err) } logger.Debugf("authorization for %#v succeeded", authInfo.Identity) c.updateDischargeTime(ctx, authInfo.Identity.Id()) if cond == "is-member-of" { return nil, nil } if p.Token != nil && len(mss) > 0 { // As well as discharging the original third party caveat, also // set the discharge token macaroon as a cookie // so that it may be used for future discharges if appropriate // (it will be ignored otherwise). if err := setIdentityCookie(p.Response, mss[0]); err != nil { return nil, errgo.Mask(err) } } var declaration checkers.Caveat switch cond { case "is-authenticated-user": declaration = candidclient.UserDeclaration(authInfo.Identity.Id()) case "is-authenticated-userid": id, ok := authInfo.Identity.(*auth.Identity) if !ok { return nil, errgo.Newf("unexpected authinfo type %T", authInfo) } declaration = candidclient.UserIDDeclaration(string(id.ProviderID)) } return []checkers.Caveat{ declaration, checkers.TimeBeforeCaveat(time.Now().Add(c.params.DischargeMacaroonTimeout)), }, nil } func macaroonsFromDischargeToken(ctx context.Context, token *httpbakery.DischargeToken) (macaroon.Slice, error) { var ms macaroon.Slice var v encoding.BinaryUnmarshaler switch token.Kind { default: return nil, errgo.WithCausef(nil, params.ErrBadRequest, "invalid token") case "agent": v = &ms case "macaroon": // TODO store a slice of macaroons in the token so // the format is the same in both cases. var m macaroon.Macaroon ms = macaroon.Slice{&m} v = &m } if err := v.UnmarshalBinary(token.Value); err != nil { return nil, errgo.WithCausef(err, params.ErrBadRequest, "invalid token") } return ms, nil } func (c *thirdPartyCaveatChecker) updateDischargeTime(ctx context.Context, username string) { err := c.params.Store.UpdateIdentity( ctx, &store.Identity{ Username: username, LastDischarge: time.Now(), }, store.Update{ store.LastDischarge: store.Set, }, ) if err != nil { logger.Infof("unexpected error updating last discharge time: %s", err) } } type interactionRequiredParams struct { forceLegacy bool why error req *http.Request info *dischargeRequestInfo dischargeID string domain string } // interactionRequiredError returns an error suitable for returning from // a discharge request that can only be satisfied if the user logs in. func (c *thirdPartyCaveatChecker) interactionRequiredError(ctx context.Context, p interactionRequiredParams) error { dischargeID, err := newDischargeID() if err != nil { return errgo.Mask(err) } // TODO(rog) If the user is already logged in (username != ""), // we should perhaps just return an error here. if err := c.place.NewRendezvous(ctx, dischargeID, p.info); err != nil { return errgo.Notef(err, "cannot make rendezvous") } ierr := httpbakery.NewInteractionRequiredError(p.why, p.req) agent.SetInteraction(ierr, agentURL(c.params.Location, dischargeID)) for _, idp := range c.params.IdentityProviders { if p.domain != "" && idp.Domain() != p.domain { // The client has specified a domain and the idp is not in that domain, // so omit it. continue } idp.SetInteraction(ierr, dischargeID) } visitParams := "?did=" + dischargeID redirectVisitParams := "" if p.domain != "" { visitParams += "&domain=" + url.QueryEscape(p.domain) redirectVisitParams = "?domain=" + url.QueryEscape(p.domain) } visitURL := c.params.Location + "/login" + visitParams waitTokenURL := c.params.Location + "/wait-token?did=" + dischargeID httpbakery.SetWebBrowserInteraction(ierr, visitURL, waitTokenURL) redirect.SetInteraction(ierr, c.params.Location+"/login-redirect"+redirectVisitParams, c.params.Location+"/discharge-token") // Set the URLs used by old clients for backward compatibility. legacyVisitURL := c.params.Location + "/login-legacy" + visitParams legacyWaitURL := c.params.Location + "/wait-legacy?did=" + dischargeID httpbakery.SetLegacyInteraction(ierr, legacyVisitURL, legacyWaitURL) if p.forceLegacy { // Even though the client might purport to support bakery V3, // they can't deal with it, so we force them to use the legacy // interaction methods by deleting all the others. ierr.Info.InteractionMethods = nil } return ierr } func isDischargeRequiredError(err error) bool { cause, ok := errgo.Cause(err).(*httpbakery.Error) return ok && cause.Code == httpbakery.ErrDischargeRequired } func newDischargeID() (string, error) { var b [32]byte if _, err := rand.Read(b[:]); err != nil { return "", errgo.Notef(err, "cannot read random bytes for discharge id") } return fmt.Sprintf("%xID", b[:]), nil } type dischargeTokenRequest struct { httprequest.Route `httprequest:"POST /discharge-token"` redirect.DischargeTokenRequest } // DischargeToken is used to collect a DischargeToken when redirect based // login is being used. func (h *handler) DischargeToken(p httprequest.Params, req *dischargeTokenRequest) (*redirect.DischargeTokenResponse, error) { var id store.Identity if err := h.params.identityStore.Get(p.Context, req.Body.Code, &id); err != nil { if errgo.Cause(err) == store.ErrNotFound { return nil, errgo.WithCausef(err, params.ErrNotFound, "") } return nil, errgo.Mask(err) } dt, err := h.params.dischargeTokenCreator.DischargeToken(p.Context, &id) if err != nil { return nil, errgo.Mask(err) } return &redirect.DischargeTokenResponse{DischargeToken: dt}, nil } golang-github-canonical-candid-1.12.3/internal/discharger/discharge_test.go000066400000000000000000000704631457263123000267550ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package discharger_test import ( "bytes" "context" "encoding/base64" "encoding/json" "io" "io/ioutil" "net/http" "net/http/cookiejar" "net/url" "path" "strings" "testing" "time" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/qthttptest" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "gopkg.in/macaroon.v2" "github.com/canonical/candid/candidclient" "github.com/canonical/candid/candidclient/redirect" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/static" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/internal/discharger" "github.com/canonical/candid/internal/identity" v1 "github.com/canonical/candid/internal/v1" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) var groupOp = bakery.Op{"group", "group"} var testContext = context.Background() func TestDischarge(t *testing.T) { qtsuite.Run(qt.New(t), &dischargeSuite{}) } type dischargeSuite struct { srv *candidtest.Server store *candidtest.Store dischargeCreator *candidtest.DischargeCreator interactor httpbakery.WebBrowserInteractor } func (s *dischargeSuite) Init(c *qt.C) { s.store = candidtest.NewStore() sp := s.store.ServerParams() sp.AdminPassword = "test-password" sp.IdentityProviders = []idp.IdentityProvider{ static.NewIdentityProvider(static.Params{ Name: "test", Domain: "", Users: map[string]static.UserInfo{ "test": { Password: "password", Name: "Test User", Email: "test@example.com", Groups: []string{"test1", "test2"}, }, "test2": { Password: "password2", Name: "Test User II", Email: "test2@example.com", }, }, Icon: "/static/idp.pcx", }), static.NewIdentityProvider(static.Params{ Name: "test-domain", Domain: "test-domain", Users: map[string]static.UserInfo{ "test": { Password: "password", Name: "Test User", Email: "test@example.com", Groups: []string{"test1", "test2"}, }, }, Icon: "/static/idp-test-domain.pcx", }), static.NewIdentityProvider(static.Params{ Name: "test-cookie-domain", Domain: "cookie-domain", Users: map[string]static.UserInfo{ "test": { Password: "password", Name: "Test User", Email: "test@example.com", Groups: []string{"test1", "test2"}, }, }, }), } sp.RedirectLoginTrustedURLs = []string{ "https://www.example.com/callback", } s.srv = candidtest.NewServer(c, sp, map[string]identity.NewAPIHandlerFunc{ "discharger": discharger.NewAPIHandler, "v1": v1.NewAPIHandler, }) s.dischargeCreator = candidtest.NewDischargeCreator(s.srv) s.interactor = httpbakery.WebBrowserInteractor{ OpenWebBrowser: candidtest.PasswordLogin(c, "test", "password"), } } func (s *dischargeSuite) TestInteractiveDischarge(c *qt.C) { s.dischargeCreator.AssertDischarge(c, s.interactor) } func (s *dischargeSuite) TestNonInteractiveDischarge(c *qt.C) { client := s.srv.AdminClient() ms, err := s.dischargeCreator.Discharge(c, "is-authenticated-user", client) c.Assert(err, qt.IsNil) s.dischargeCreator.AssertMacaroon(c, ms, identchecker.LoginOp, auth.AdminUsername) } func (s *dischargeSuite) TestInteractiveDischargeWithOldClientCaveat(c *qt.C) { ms, err := s.dischargeCreator.Discharge(c, " 1, qt.Equals, true) // do normal interactive login return s.interactor.OpenWebBrowser(u) } client := s.srv.Client(httpbakery.WebBrowserInteractor{ OpenWebBrowser: openWebBrowser, }) _, err := s.dischargeCreator.Discharge(c, "is-authenticated-user", client) c.Assert(err, qt.IsNil) } func (s *dischargeSuite) TestTwoDischargesOfSameCaveat(c *qt.C) { // First make start an interaction-required discharge, but don't // allow it to complete immediately. interacting := make(chan struct{}) done := make(chan struct{}) // Create a macaroon that we'll try to discharge twice concurrently. m := s.dischargeCreator.NewMacaroon(c, "is-authenticated-user", identchecker.LoginOp) go func() { client := s.srv.Client(httpbakery.WebBrowserInteractor{ OpenWebBrowser: func(u *url.URL) error { interacting <- struct{}{} <-interacting return s.interactor.OpenWebBrowser(u) }, }) ms, err := client.DischargeAll(s.srv.Ctx, m) c.Check(err, qt.Equals, nil) _, err = s.dischargeCreator.Bakery.Checker.Auth(ms).Allow(context.Background(), identchecker.LoginOp) c.Check(err, qt.Equals, nil) close(done) }() <-interacting // The first discharge is now stuck in OpenWebBrowser until we // tell it to go ahead, so try to discharge the same macaroon that // we just tried. client := s.srv.Client(s.interactor) ms, err := client.DischargeAll(s.srv.Ctx, m) c.Check(err, qt.Equals, nil) _, err = s.dischargeCreator.Bakery.Checker.Auth(ms).Allow(context.Background(), identchecker.LoginOp) c.Check(err, qt.Equals, nil) // Let the other one proceed - it should succeed too. interacting <- struct{}{} <-done } func (s *dischargeSuite) TestDischargeWhenLoggedIn(c *qt.C) { client := s.srv.Client(s.interactor) ms, err := s.dischargeCreator.Discharge(c, "is-authenticated-user", client) c.Assert(err, qt.IsNil) s.dischargeCreator.AssertMacaroon(c, ms, identchecker.LoginOp, "test") ms, err = s.dischargeCreator.Discharge(c, "is-authenticated-user", client) c.Assert(err, qt.IsNil) s.dischargeCreator.AssertMacaroon(c, ms, identchecker.LoginOp, "test") } func (s *dischargeSuite) TestVisitURLWithDomainCookie(c *qt.C) { u, err := url.Parse(s.srv.URL + "/discharge") c.Assert(err, qt.IsNil) client := s.srv.Client(nil) client.Client.Jar.SetCookies(u, []*http.Cookie{{ Name: "domain", Value: "test2", }}) openWebBrowser := &valueSavingOpenWebBrowser{ openWebBrowser: s.interactor.OpenWebBrowser, } client.AddInteractor(httpbakery.WebBrowserInteractor{ OpenWebBrowser: openWebBrowser.OpenWebBrowser, }) _, err = s.dischargeCreator.Discharge(c, "is-authenticated-user", client) c.Assert(err, qt.IsNil) c.Assert(openWebBrowser.url.Query().Get("domain"), qt.Equals, "test2") } func (s *dischargeSuite) TestVisitURLWithInvalidDomainCookie(c *qt.C) { u, err := url.Parse(s.srv.URL + "/discharge") c.Assert(err, qt.IsNil) client := s.srv.Client(nil) client.Client.Jar.SetCookies(u, []*http.Cookie{{ Name: "domain", Value: "test2-", }}) openWebBrowser := &valueSavingOpenWebBrowser{ openWebBrowser: s.interactor.OpenWebBrowser, } client.AddInteractor(httpbakery.WebBrowserInteractor{ OpenWebBrowser: openWebBrowser.OpenWebBrowser, }) _, err = s.dischargeCreator.Discharge(c, "is-authenticated-user", client) c.Assert(err, qt.IsNil) c.Assert(openWebBrowser.url.Query().Get("domain"), qt.Equals, "") } func (s *dischargeSuite) TestVisitURLWithEscapedDomainCookie(c *qt.C) { u, err := url.Parse(s.srv.URL + "/discharge") c.Assert(err, qt.IsNil) client := s.srv.Client(nil) client.Client.Jar.SetCookies(u, []*http.Cookie{{ Name: "domain", Value: "test+2", }}) openWebBrowser := &valueSavingOpenWebBrowser{ openWebBrowser: s.interactor.OpenWebBrowser, } client.AddInteractor(httpbakery.WebBrowserInteractor{ OpenWebBrowser: openWebBrowser.OpenWebBrowser, }) _, err = s.dischargeCreator.Discharge(c, "is-authenticated-user", client) c.Assert(err, qt.IsNil) c.Assert(openWebBrowser.url.Query().Get("domain"), qt.Equals, "test+2") } // cookiesToMacaroons returns a slice of any macaroons found // in the given slice of cookies. func cookiesToMacaroons(cookies []*http.Cookie) []macaroon.Slice { var mss []macaroon.Slice for _, cookie := range cookies { if !strings.HasPrefix(cookie.Name, "macaroon-") { continue } ms, err := decodeMacaroonSlice(cookie.Value) if err != nil { continue } mss = append(mss, ms) } return mss } // decodeMacaroonSlice decodes a base64-JSON-encoded slice of macaroons from // the given string. func decodeMacaroonSlice(value string) (macaroon.Slice, error) { data, err := base64.StdEncoding.DecodeString(value) if err != nil { return nil, errgo.NoteMask(err, "cannot base64-decode macaroons") } var ms macaroon.Slice if err := json.Unmarshal(data, &ms); err != nil { return nil, errgo.NoteMask(err, "cannot unmarshal macaroons") } return ms, nil } func isVerificationError(err error) bool { _, ok := err.(*bakery.VerificationError) return ok } type responseBody struct { url *url.URL body []byte header http.Header } type responseBodyRecordingTransport struct { c *qt.C transport http.RoundTripper responses []responseBody } func (t *responseBodyRecordingTransport) RoundTrip(req *http.Request) (*http.Response, error) { transport := t.transport if transport == nil { transport = &http.Transport{} } resp, err := transport.RoundTrip(req) if err != nil { return nil, err } var buf bytes.Buffer io.Copy(&buf, resp.Body) resp.Body = ioutil.NopCloser(&buf) if resp.StatusCode == 200 { t.responses = append(t.responses, responseBody{ url: req.URL, body: buf.Bytes(), }) } return resp, nil } func (s *dischargeSuite) TestDischargeFromDifferentOriginWhenLoggedIn(c *qt.C) { c.Skip("origin caveats on identity cookies not yet supported") var disabled bool openWebBrowser := func(u *url.URL) error { if disabled { return errgo.New("visit required but not allowed") } return s.interactor.OpenWebBrowser(u) } client := s.srv.Client(httpbakery.WebBrowserInteractor{ OpenWebBrowser: openWebBrowser, }) _, err := s.dischargeCreator.Discharge(c, "is-authenticated-user", client) c.Assert(err, qt.IsNil) disabled = true _, err = s.dischargeCreator.Discharge(c, "is-authenticated-user", client) c.Assert(err, qt.IsNil) // Check that we can't discharge using the candid macaroon // when we've got a different origin header. client.Transport = originTransport{client.Transport, "somewhere"} _, err = s.dischargeCreator.Discharge(c, "is-authenticated-user", client) // TODO this error doesn't seem that closely related to the test failure condition. c.Assert(err, qt.ErrorMatches, `cannot get discharge from ".*": cannot start interactive session: unexpected call to visit`) } type originTransport struct { transport http.RoundTripper origin string } func (t originTransport) RoundTrip(req *http.Request) (*http.Response, error) { h := make(http.Header) for attr, val := range req.Header { h[attr] = val } h.Set("Origin", t.origin) req1 := *req req1.Header = h transport := t.transport if transport == nil { transport = &http.Transport{} } return transport.RoundTrip(&req1) } var dischargeForUserTests = []struct { about string condition string username string password string dischargeForUser string m *bakery.Macaroon expectUser string expectErr string }{{ about: "discharge macaroon", condition: "is-authenticated-user", username: "admin", password: "test-password", dischargeForUser: "jbloggs", expectUser: "jbloggs", }, { about: "no authentication", condition: "is-authenticated-user", dischargeForUser: "jbloggs", expectErr: `cannot get discharge from ".*": Post .*/discharge: macaroon discharge required: authentication required`, }, { about: "unsupported user", condition: "is-authenticated-user", username: "admin", password: "test-password", dischargeForUser: "jbloggs2", expectErr: `cannot get discharge from ".*": Post .*/discharge: cannot discharge: could not determine identity: user jbloggs2 not found`, }, { about: "unsupported condition", condition: "is-authenticated-group", username: "admin", password: "test-password", dischargeForUser: "jbloggs", expectErr: `.*caveat not recognized`, }, { about: "bad credentials", condition: "is-authenticated-user", username: "not-admin-username", password: "test-password", dischargeForUser: "jbloggs", expectErr: `cannot get discharge from ".*": Post .*/discharge: cannot discharge: could not determine identity: invalid credentials`, }, { about: "is-authenticated-user with domain", condition: "is-authenticated-user @test", username: "admin", password: "test-password", dischargeForUser: "jbloggs@test", expectUser: "jbloggs@test", }, { about: "is-authenticated-user with wrong domain", condition: "is-authenticated-user @test2", username: "admin", password: "test-password", dischargeForUser: "jbloggs@test", expectErr: `cannot get discharge from ".*": Post .*/discharge: cannot discharge: could not determine identity: "jbloggs@test" not in required domain "test2"`, }, { about: "is-authenticated-user with invalid domain", condition: "is-authenticated-user @test-", username: "admin", password: "test-password", dischargeForUser: "jbloggs@test", expectErr: `cannot get discharge from ".*": Post .*/discharge: cannot discharge: invalid domain "test-"`, }, { about: "invalid caveat", condition: " invalid caveat", username: "admin", password: "test-password", dischargeForUser: "jbloggs@test", expectErr: `cannot get discharge from ".*": Post .*/discharge: cannot discharge: cannot parse caveat " invalid caveat": caveat starts with space character`, }} func (s *dischargeSuite) TestDischargeForUser(c *qt.C) { s.srv.CreateUser(c, "jbloggs", "test") s.srv.CreateUser(c, "jbloggs@test", "test") for i, test := range dischargeForUserTests { c.Logf("test %d. %s", i, test.about) da := &testDischargeAcquirer{ client: &httprequest.Client{ BaseURL: s.srv.URL, }, username: test.username, password: test.password, dischargeForUser: test.dischargeForUser, } ms, err := bakery.DischargeAll( s.srv.Ctx, s.dischargeCreator.NewMacaroon(c, test.condition, identchecker.LoginOp), da.AcquireDischarge, ) if test.expectErr != "" { c.Assert(err, qt.ErrorMatches, test.expectErr) continue } c.Assert(err, qt.IsNil) ui, err := s.dischargeCreator.Bakery.Checker.Auth(ms).Allow(context.Background(), identchecker.LoginOp) c.Assert(ui.Identity.Id(), qt.Equals, test.expectUser) } } // testDischargeAcquirer acquires a discharge by using the provided basic // authentication credentials to perform an discharge as the specified // user. type testDischargeAcquirer struct { client *httprequest.Client username, password string dischargeForUser string } func (da *testDischargeAcquirer) AcquireDischarge(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { u, err := url.Parse(cav.Location) if err != nil { return nil, errgo.Mask(err) } u.Path = path.Join(u.Path, "discharge") params := make(url.Values) if len(payload) > 0 { params.Set("id64", base64.RawURLEncoding.EncodeToString(cav.Id)) params.Set("caveat64", base64.RawURLEncoding.EncodeToString(payload)) } else { params.Set("id64", base64.RawURLEncoding.EncodeToString(cav.Id)) } if da.dischargeForUser != "" { params.Set("discharge-for-user", da.dischargeForUser) } req, err := http.NewRequest("POST", u.String(), strings.NewReader(params.Encode())) if err != nil { return nil, errgo.Mask(err) } if da.username != "" { req.SetBasicAuth(da.username, da.password) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") var dr dischargeResponse if err := da.client.Do(ctx, req, &dr); err != nil { return nil, errgo.Mask(err, errgo.Any) } return dr.Macaroon, nil } type dischargeResponse struct { Macaroon *bakery.Macaroon `json:",omitempty"` } var dischargeMemberOfTests = []struct { name string condition string expectError string }{{ name: "SingleGroupsIsUsername", condition: "is-member-of test", }, { name: "ManyGroupsOneIsUsername", condition: "is-member-of test testX testY", }, { name: "SingleGroupMatch", condition: "is-member-of test1", }, { name: "ManyGroupsAllMatch", condition: "is-member-of test1 test2", }, { name: "SingleGroupNoMatch", condition: "is-member-of test3", expectError: `cannot get discharge from ".*": Post http.*: permission denied`, }, { name: "ManyGroupsOneMatches", condition: "is-member-of test2 test4", }, { name: "ManyGroupsNoMatch", condition: "is-member-of test3 test4", expectError: `cannot get discharge from ".*": Post http.*: permission denied`, }} func (s *dischargeSuite) TestDischargeMemberOf(c *qt.C) { client := s.srv.Client(s.interactor) ctx := context.Background() for _, test := range dischargeMemberOfTests { c.Run(test.name, func(c *qt.C) { m := s.dischargeCreator.NewMacaroon(c, test.condition, groupOp) ms, err := client.DischargeAll(ctx, m) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) return } c.Assert(err, qt.IsNil) s.dischargeCreator.AssertMacaroon(c, ms, groupOp, "") }) } } func (s *dischargeSuite) TestDischargeXMemberOfX(c *qt.C) { // if the user is X member of no group, we must still // discharge is-member-of X. client := s.srv.Client(httpbakery.WebBrowserInteractor{ OpenWebBrowser: candidtest.PasswordLogin(c, "test2", "password2"), }) m := s.dischargeCreator.NewMacaroon(c, "is-member-of test2", groupOp) ms, err := client.DischargeAll(context.Background(), m) c.Assert(err, qt.IsNil) s.dischargeCreator.AssertMacaroon(c, ms, groupOp, "") } // This test is not sending the bakery protocol version so it will use the default // one and return a 407. func (s *dischargeSuite) TestDischargeStatusProxyAuthRequiredResponse(c *qt.C) { // Make a version 1 macaroon so that the caveat is in the macaroon // and it's appropriate for a 407-era macaroon. m, err := s.dischargeCreator.Bakery.Oven.NewMacaroon( testContext, bakery.Version1, []checkers.Caveat{{ Location: s.srv.URL, Condition: "is-authenticated-user", }}, identchecker.LoginOp, ) c.Assert(err, qt.IsNil) var thirdPartyCaveat macaroon.Caveat for _, cav := range m.M().Caveats() { if cav.VerificationId != nil { thirdPartyCaveat = cav break } } c.Assert(thirdPartyCaveat.Id, qt.Not(qt.Equals), "") resp, err := http.PostForm(s.srv.URL+"/discharge", url.Values{ "id": {string(thirdPartyCaveat.Id)}, "location": {thirdPartyCaveat.Location}, }) c.Assert(err, qt.IsNil) defer resp.Body.Close() c.Assert(resp.StatusCode, qt.Equals, http.StatusProxyAuthRequired) } // This test is using the bakery protocol version at value 1 to be able to return a 401 // instead of a 407 func (s *dischargeSuite) TestDischargeStatusUnauthorizedResponse(c *qt.C) { // Make a version 2 macaroon so that the caveat is in the macaroon. m, err := s.dischargeCreator.Bakery.Oven.NewMacaroon( testContext, bakery.Version2, []checkers.Caveat{{ Location: s.srv.URL, Condition: "is-authenticated-user", }}, identchecker.LoginOp, ) c.Assert(err, qt.IsNil) var thirdPartyCaveat macaroon.Caveat for _, cav := range m.M().Caveats() { if cav.VerificationId != nil { thirdPartyCaveat = cav break } } c.Assert(thirdPartyCaveat.Id, qt.Not(qt.Equals), "") values := url.Values{ "id": {string(thirdPartyCaveat.Id)}, "location": {thirdPartyCaveat.Location}, } req, err := http.NewRequest("POST", s.srv.URL+"/discharge", strings.NewReader(values.Encode())) c.Assert(err, qt.IsNil) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Bakery-Protocol-Version", "1") resp, err := http.DefaultClient.Do(req) c.Assert(err, qt.IsNil) defer resp.Body.Close() c.Assert(resp.StatusCode, qt.Equals, http.StatusUnauthorized) c.Assert(resp.Header.Get("WWW-Authenticate"), qt.Equals, "Macaroon") } func (s *dischargeSuite) TestPublicKey(c *qt.C) { info, err := s.srv.ThirdPartyInfo(testContext, s.srv.URL) c.Assert(err, qt.IsNil) qthttptest.AssertJSONCall(c, qthttptest.JSONCallParams{ URL: s.srv.URL + "/publickey", ExpectStatus: http.StatusOK, ExpectBody: map[string]*bakery.PublicKey{ "PublicKey": &info.PublicKey, }, }) } func (s *dischargeSuite) TestIdentityCookieParameters(c *qt.C) { client := s.srv.Client(s.interactor) jar := new(testCookieJar) client.Client.Jar = jar ms, err := s.dischargeCreator.Discharge(c, "is-authenticated-user", client) c.Assert(err, qt.IsNil) s.dischargeCreator.AssertMacaroon(c, ms, identchecker.LoginOp, "test") c.Assert(jar.cookies, qt.HasLen, 1) for k := range jar.cookies { c.Assert(k.name, qt.Equals, "macaroon-identity") c.Assert(k.path, qt.Equals, "/") } } type cookieKey struct { domain string path string name string } type testCookieJar struct { cookies map[cookieKey]*http.Cookie } func (j *testCookieJar) SetCookies(u *url.URL, cs []*http.Cookie) { if j.cookies == nil { j.cookies = make(map[cookieKey]*http.Cookie) } for _, c := range cs { key := cookieKey{ domain: u.Host, path: u.Path, name: c.Name, } if c.Domain != "" { key.domain = c.Domain } if c.Path != "" { key.path = c.Path } j.cookies[key] = c } } func (j *testCookieJar) Cookies(u *url.URL) []*http.Cookie { return nil } func (s *dischargeSuite) TestLastDischargeTimeUpdates(c *qt.C) { s.dischargeCreator.AssertDischarge(c, s.interactor) id1 := store.Identity{ ProviderID: "test:test", } err := s.store.Store.Identity(context.Background(), &id1) c.Assert(err, qt.IsNil) c.Assert(id1.LastDischarge.IsZero(), qt.Equals, false) // Wait at least one ms so that the discharge time stored in the // database is necessarily different. time.Sleep(time.Millisecond) s.dischargeCreator.AssertDischarge(c, s.interactor) id2 := store.Identity{ ProviderID: "test:test", } err = s.store.Store.Identity(context.Background(), &id2) c.Assert(err, qt.IsNil) c.Assert(id2.LastDischarge.After(id1.LastDischarge), qt.Equals, true) } var domainInteractionURLTests = []struct { about string condition string cookies map[string]string expectDomain string }{{ about: "domain login", condition: "is-authenticated-user @test-domain", expectDomain: "test-domain", }, { about: "no domain", condition: "is-authenticated-user", }, { about: "domain from cookies", condition: "is-authenticated-user", cookies: map[string]string{ "domain": "cookie-domain", }, expectDomain: "cookie-domain", }, { about: "condition trumps cookies", condition: "is-authenticated-user @test-domain", cookies: map[string]string{ "domain": "cookie-domain", }, expectDomain: "test-domain", }} func (s *dischargeSuite) TestDomainInInteractionURLs(c *qt.C) { for _, tst := range domainInteractionURLTests { c.Run(tst.about, func(c *qt.C) { client := s.srv.Client(s.interactor) for k, v := range tst.cookies { u, err := url.Parse(s.srv.URL) c.Assert(err, qt.IsNil) client.Jar.SetCookies(u, []*http.Cookie{{ Name: k, Value: v, }}) } ms, err := s.dischargeCreator.Discharge(c, tst.condition, client) c.Assert(err, qt.IsNil) username := "test" if tst.expectDomain != "" { username = "test@" + tst.expectDomain } s.dischargeCreator.AssertMacaroon(c, ms, identchecker.LoginOp, username) }) } } func (s *dischargeSuite) TestDischargeWithDomainWithExistingNonDomainAuth(c *qt.C) { // First log in successfully without a domain. s.dischargeCreator.AssertDischarge(c, s.interactor) // Then try with a caveat that requires a domain. ms, err := s.dischargeCreator.Discharge(c, "is-authenticated-user @test-domain", s.srv.Client(s.interactor)) c.Assert(err, qt.IsNil) s.dischargeCreator.AssertMacaroon(c, ms, identchecker.LoginOp, "test@test-domain") } type valueSavingOpenWebBrowser struct { url *url.URL openWebBrowser func(u *url.URL) error } func (v *valueSavingOpenWebBrowser) OpenWebBrowser(u *url.URL) error { v.url = u return v.openWebBrowser(u) } func (s *dischargeSuite) TestDischargeBrowserRedirectLogin(c *qt.C) { interactor := new(redirect.Interactor) _, err := s.dischargeCreator.Discharge(c, "is-authenticated-user", s.srv.Client(interactor)) c.Assert(httpbakery.IsInteractionError(errgo.Cause(err)), qt.Equals, true, qt.Commentf("%v", errgo.Details(errgo.Cause(err)))) ierr := errgo.Cause(err).(*httpbakery.InteractionError) c.Assert(redirect.IsRedirectRequiredError(errgo.Cause(ierr.Reason)), qt.Equals, true) rerr := errgo.Cause(ierr.Reason).(*redirect.RedirectRequiredError) jar, err := cookiejar.New(nil) c.Assert(err, qt.IsNil) client := &http.Client{ Jar: jar, CheckRedirect: func(req *http.Request, via []*http.Request) error { if req.URL.Host == "www.example.com" { return http.ErrUseLastResponse } return nil }, } resp, err := client.Get(rerr.InteractionInfo.RedirectURL("https://www.example.com/callback", "123456")) c.Assert(err, qt.IsNil) f := candidtest.SelectInteractiveLogin(candidtest.PostLoginForm("test", "password")) resp, err = f(client, resp) c.Assert(err, qt.IsNil) defer resp.Body.Close() c.Assert(resp.StatusCode, qt.Equals, http.StatusSeeOther, qt.Commentf("unexpected response %q", resp.Status)) state, code, err := redirect.ParseLoginResult(resp.Header.Get("Location")) c.Assert(err, qt.IsNil) c.Assert(state, qt.Equals, "123456") dt, err := rerr.InteractionInfo.GetDischargeToken(context.Background(), code) c.Assert(err, qt.IsNil) interactor.SetDischargeToken(rerr.InteractionInfo.LoginURL, dt) ms, err := s.dischargeCreator.Discharge(c, "is-authenticated-user", s.srv.Client(interactor)) c.Assert(err, qt.IsNil) s.dischargeCreator.AssertMacaroon(c, ms, identchecker.LoginOp, "") } func (s *dischargeSuite) TestDischargeBrowserRedirectLoginNotTrusted(c *qt.C) { interactor := new(redirect.Interactor) _, err := s.dischargeCreator.Discharge(c, "is-authenticated-user", s.srv.Client(interactor)) c.Assert(httpbakery.IsInteractionError(errgo.Cause(err)), qt.Equals, true, qt.Commentf("%v", errgo.Details(errgo.Cause(err)))) ierr := errgo.Cause(err).(*httpbakery.InteractionError) c.Assert(redirect.IsRedirectRequiredError(errgo.Cause(ierr.Reason)), qt.Equals, true) rerr := errgo.Cause(ierr.Reason).(*redirect.RedirectRequiredError) jar, err := cookiejar.New(nil) c.Assert(err, qt.IsNil) client := &http.Client{ Jar: jar, CheckRedirect: func(req *http.Request, via []*http.Request) error { if req.URL.Host == "www.example.com" { return http.ErrUseLastResponse } return nil }, } resp, err := client.Get(rerr.InteractionInfo.RedirectURL("https://www.example.com/callback2", "123456")) c.Assert(err, qt.IsNil) f := candidtest.SelectInteractiveLogin(candidtest.PostLoginForm("test", "password")) resp, err = f(client, resp) c.Assert(err, qt.IsNil) defer resp.Body.Close() c.Assert(resp.StatusCode, qt.Equals, http.StatusBadRequest, qt.Commentf("unexpected response %q", resp.Status)) var perr params.Error err = httprequest.UnmarshalJSONResponse(resp, &perr) c.Assert(err, qt.IsNil) c.Assert(&perr, qt.ErrorMatches, `invalid return_to "https://www.example.com/callback2"`) } func (s *dischargeSuite) TestDischargeUserID(c *qt.C) { dc := candidtest.NewUserIDDischargeCreator(s.srv) client := s.srv.AdminClient() ms, err := dc.Discharge(c, "is-authenticated-userid", client) c.Assert(err, qt.IsNil) ai, err := dc.Bakery.Checker.Auth(ms).Allow(context.Background(), identchecker.LoginOp) c.Assert(err, qt.IsNil) c.Assert(ai.Identity.Id(), qt.Equals, string(auth.AdminProviderID)) id, ok := ai.Identity.(candidclient.Identity) c.Assert(ok, qt.Equals, true) username, err := id.Username() c.Assert(err, qt.IsNil) c.Assert(username, qt.Equals, auth.AdminUsername) } golang-github-canonical-candid-1.12.3/internal/discharger/export_test.go000066400000000000000000000012441457263123000263340ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package discharger import ( "github.com/juju/simplekv" "github.com/canonical/candid/idp" "github.com/canonical/candid/internal/discharger/internal" "github.com/canonical/candid/internal/identity" "github.com/canonical/candid/store" ) var NewIDPHandler = newIDPHandler type LoginInfo loginInfo func NewVisitCompleter(params identity.HandlerParams, kvstore simplekv.Store, store store.Store) idp.VisitCompleter { return &visitCompleter{ params: params, identityStore: internal.NewIdentityStore(kvstore, store), place: &place{params.MeetingPlace}, } } golang-github-canonical-candid-1.12.3/internal/discharger/idp.go000066400000000000000000000243401457263123000245320ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package discharger import ( "context" "fmt" "net/http" "net/url" "strings" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/julienschmidt/httprouter" "golang.org/x/net/trace" "gopkg.in/errgo.v1" macaroon "gopkg.in/macaroon.v2" "github.com/canonical/candid/candidclient" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/idputil/secret" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/internal/discharger/internal" "github.com/canonical/candid/internal/identity" "github.com/canonical/candid/internal/mfa" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) type initIDPParams struct { identity.HandlerParams Codec *secret.Codec DischargeTokenCreator *dischargeTokenCreator VisitCompleter *visitCompleter } func initIDPs(ctx context.Context, params initIDPParams) error { for _, ip := range params.IdentityProviders { kvStore, err := params.ProviderDataStore.KeyValueStore(ctx, ip.Name()) if err != nil { return errgo.Mask(err) } if err := ip.Init(ctx, idp.InitParams{ Store: params.Store, KeyValueStore: kvStore, Oven: params.Oven, Codec: params.Codec, Location: params.Location, URLPrefix: params.Location + "/login/" + ip.Name(), DischargeTokenCreator: params.DischargeTokenCreator, VisitCompleter: params.VisitCompleter, Template: params.Template, SkipLocationForCookiePaths: params.SkipLocationForCookiePaths, }); err != nil { return errgo.Mask(err) } } return nil } func newIDPHandler(params identity.HandlerParams, idp idp.IdentityProvider) httprouter.Handle { return func(w http.ResponseWriter, req *http.Request, p httprouter.Params) { t := trace.New("identity.internal.v1.idp", idp.Name()) defer t.Finish() ctx := trace.NewContext(context.Background(), t) ctx, close := params.Store.Context(ctx) defer close() ctx, close = params.MeetingStore.Context(ctx) defer close() req.URL.Path = strings.TrimPrefix(req.URL.Path, "/login/"+idp.Name()) req.ParseForm() idp.Handle(ctx, w, req) } } func newMFAHandler(params identity.HandlerParams, authorizer *mfa.Authenticator) httprouter.Handle { return func(w http.ResponseWriter, req *http.Request, p httprouter.Params) { t := trace.New("identity.internal.v1", "mfa") defer t.Finish() ctx := trace.NewContext(context.Background(), t) ctx, close := params.Store.Context(ctx) defer close() ctx, close = params.MeetingStore.Context(ctx) defer close() req.URL.Path = strings.TrimPrefix(req.URL.Path, "/login/mfa") req.ParseForm() authorizer.Handle(ctx, w, req) } } type dischargeTokenCreator struct { params identity.HandlerParams } func (d *dischargeTokenCreator) DischargeToken(ctx context.Context, id *store.Identity) (*httpbakery.DischargeToken, error) { m, err := d.params.Oven.NewMacaroon( ctx, bakery.LatestVersion, []checkers.Caveat{ checkers.TimeBeforeCaveat(time.Now().Add(d.params.DischargeTokenTimeout)), candidclient.UserIDDeclaration(string(id.ProviderID)), }, identchecker.LoginOp, ) if err != nil { return nil, errgo.Mask(err) } v, err := m.M().MarshalBinary() if err != nil { return nil, errgo.Mask(err) } id.LastLogin = time.Now() if err := d.params.Store.UpdateIdentity(ctx, id, store.Update{ store.LastLogin: store.Set, }); err != nil { logger.Errorf("cannot update last login time: %s", err) } return &httpbakery.DischargeToken{ Kind: "macaroon", Value: v, }, nil } // MFAAuthenticator defines the interface used by the visitCompleter to // interact with the MFA authentication flow. type MFAAuthenticator interface { // SetMFAStateProviderID sets the provider id in the mfa login state cookie. SetMFAStateProviderID(w http.ResponseWriter, providerID string) (string, error) // HasMFACredentials returns true, if the user with the specified providerID has // any registered MFA credentials. HasMFACredentials(ctx context.Context, providerID string) (bool, error) } // A visitCompleter is an implementation of idp.VisitCompleter. type visitCompleter struct { params identity.HandlerParams identityStore *internal.IdentityStore place *place mfaAuthenticator MFAAuthenticator } type idWithMFACredentials struct { *store.Identity params.TemplateBrandParameters ManageURL string } func (c *visitCompleter) manageURL(ctx context.Context, w http.ResponseWriter, id *store.Identity) (string, error) { if c.mfaAuthenticator == nil { return "", errgo.New("MFA authenticator not specified") } hasCredentials, err := c.mfaAuthenticator.HasMFACredentials(ctx, string(id.ProviderID)) if err != nil { return "", errgo.Notef(err, "failed to retrieve use MFA credentials") } if !hasCredentials { return "", nil } var mfaState string mfaState, err = c.mfaAuthenticator.SetMFAStateProviderID(w, string(id.ProviderID)) if err != nil { return "", errgo.Notef(err, "failed to set MFA state") } v := url.Values{} if mfaState != "" { v.Set(mfa.StateName, mfaState) } return c.params.Location + "/login/mfa/manage?" + v.Encode(), nil } // Success implements idp.VisitCompleter.Success. func (c *visitCompleter) Success(ctx context.Context, w http.ResponseWriter, req *http.Request, dischargeID string, id *store.Identity) { if dischargeID != "" { if err := c.place.Done(ctx, dischargeID, &loginInfo{ProviderID: id.ProviderID}); err != nil { c.Failure(ctx, w, req, dischargeID, errgo.Mask(err)) return } } data := idWithMFACredentials{ Identity: id, } manageURL, err := c.manageURL(ctx, w, id) if err != nil { logger.Warningf(err.Error()) } data.ManageURL = manageURL t := c.params.Template.Lookup("login") if t == nil { fmt.Fprintf(w, "Login successful as %s", id.Username) return } w.Header().Set("Content-Type", "text/html;charset=utf-8") data.TemplateBrandParameters = params.BrandParameters() if err := t.Execute(w, data); err != nil { logger.Errorf("error processing login template: %s", err) } } // Failure implements idp.VisitCompleter.Failure. func (c *visitCompleter) Failure(ctx context.Context, w http.ResponseWriter, req *http.Request, dischargeID string, err error) { _, bakeryErr := httpbakery.ErrorToResponse(ctx, err) if dischargeID != "" { c.place.Done(ctx, dischargeID, &loginInfo{ Error: bakeryErr.(*httpbakery.Error), }) } identity.WriteError(ctx, w, err) } // RedirectSuccess implements idp.VisitCompleter.RedirectSuccess. func (c *visitCompleter) RedirectSuccess(ctx context.Context, w http.ResponseWriter, req *http.Request, returnTo, state string, id *store.Identity) { code, err := c.identityStore.Put(ctx, id, time.Now().Add(10*time.Minute)) if err != nil { c.RedirectFailure(ctx, w, req, returnTo, state, errgo.Mask(err)) return } v := url.Values{ "code": {code}, } if state != "" { v.Set("state", state) } if err := c.redirect(w, req, returnTo, v); err != nil { identity.WriteError(ctx, w, err) } return } // RedirectMFA implements idp.VisitCompleter.RedirectMFA. func (c *visitCompleter) RedirectMFA(ctx context.Context, w http.ResponseWriter, req *http.Request, requireMFA bool, returnTo, returnToState, state string, id *store.Identity) { if !requireMFA { c.RedirectSuccess(ctx, w, req, returnTo, returnToState, id) return } if c.mfaAuthenticator == nil { c.RedirectFailure(ctx, w, req, returnTo, returnToState, errgo.New("invalid mfa configuration")) return } mfaState, err := c.mfaAuthenticator.SetMFAStateProviderID(w, string(id.ProviderID)) if err != nil { c.RedirectFailure(ctx, w, req, returnTo, returnToState, err) return } v := url.Values{} if state != "" { v.Set("state", state) } if mfaState != "" { v.Set(mfa.StateName, mfaState) } if err := c.redirect(w, req, c.params.Location+"/login/mfa/login", v); err != nil { identity.WriteError(ctx, w, err) } return } // RedirectFailure implements idp.VisitCompleter.RedirectFailure. func (c *visitCompleter) RedirectFailure(ctx context.Context, w http.ResponseWriter, req *http.Request, returnTo, state string, err error) { v := url.Values{ "error": {err.Error()}, } if state != "" { v.Set("state", state) } if ec, ok := errgo.Cause(err).(params.ErrorCode); ok { v.Set("error_code", string(ec)) } if rerr := c.redirect(w, req, returnTo, v); rerr == nil { return } identity.WriteError(ctx, w, err) } // redirect writes a redirect response addressed the the given returnTo // address with the given query parameters. If an error is returned it // will be because the returnTo address is invalid and therefore it will // not be possible to redirect to it. func (c *visitCompleter) redirect(w http.ResponseWriter, req *http.Request, returnTo string, query url.Values) error { // Check the return to is a valid URL and is an allowed address. u, err := url.Parse(returnTo) if err != nil || !c.isValidReturnTo(u) { return errgo.WithCausef(err, params.ErrBadRequest, "invalid return_to %q", returnTo) } q := u.Query() for k, v := range query { q[k] = append(q[k], v...) } u.RawQuery = q.Encode() http.Redirect(w, req, u.String(), http.StatusSeeOther) return nil } func (c *visitCompleter) isValidReturnTo(u *url.URL) bool { s := u.String() if s == c.params.Location+"/login-complete" { return true } if s == c.params.Location+"/login/mfa/login" { return true } for _, rurl := range c.params.RedirectLoginTrustedURLs { if s == rurl { return true } } if u.Scheme != "https" { return false } for _, d := range c.params.RedirectLoginTrustedDomains { if strings.HasPrefix(d, "*.") && strings.HasSuffix(u.Host, d[1:]) { return true } else if u.Host == d { return true } } return false } func usernameFromDischargeToken(dt *httpbakery.DischargeToken) string { if dt.Kind != "macaroon" { return "" } var m macaroon.Macaroon if err := m.UnmarshalBinary(dt.Value); err != nil { return "" } return checkers.InferDeclared(auth.Namespace, macaroon.Slice{&m})["username"] } golang-github-canonical-candid-1.12.3/internal/discharger/idp_test.go000066400000000000000000000221031457263123000255640ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package discharger_test import ( "context" "encoding/json" "html/template" "io/ioutil" "net/http" "net/http/httptest" "testing" "time" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/internal/discharger" "github.com/canonical/candid/internal/identity" "github.com/canonical/candid/internal/monitoring" "github.com/canonical/candid/meeting" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) func TestIDP(t *testing.T) { qtsuite.Run(qt.New(t), &idpSuite{}) } type idpSuite struct { store *candidtest.Store // template is used to configure the output generated by success // following a login. if there is a template called "login" in // template then it will be processed and the output returned. template *template.Template meetingPlace *meeting.Place vc idp.VisitCompleter } func (s *idpSuite) Init(c *qt.C) { s.store = candidtest.NewStore() s.template = template.New("") oven := bakery.NewOven(bakery.OvenParams{ Namespace: auth.Namespace, RootKeyStoreForOps: func([]bakery.Op) bakery.RootKeyStore { return s.store.BakeryRootKeyStore }, Key: bakery.MustGenerateKey(), Location: "candidtest", }) var err error s.meetingPlace, err = meeting.NewPlace(meeting.Params{ Store: s.store.MeetingStore, Metrics: monitoring.NewMeetingMetrics(), ListenAddr: "localhost", }) c.Assert(err, qt.IsNil) c.Defer(s.meetingPlace.Close) kvs, err := s.store.ProviderDataStore.KeyValueStore(context.Background(), "test-discharge-tokens") c.Assert(err, qt.IsNil) s.vc = discharger.NewVisitCompleter(identity.HandlerParams{ ServerParams: identity.ServerParams{ Store: s.store.Store, MeetingStore: s.store.MeetingStore, RootKeyStore: s.store.BakeryRootKeyStore, Template: s.template, RedirectLoginTrustedURLs: []string{ "http://example.com/callback", }, RedirectLoginTrustedDomains: []string{ "www.example.net", "*.example.org", }, }, MeetingPlace: s.meetingPlace, Oven: oven, }, kvs, s.store.Store) } func (s *idpSuite) TestLoginFailure(c *qt.C) { rr := httptest.NewRecorder() s.vc.Failure(context.Background(), rr, nil, "", errgo.WithCausef(nil, params.ErrForbidden, "test error")) c.Assert(rr.Code, qt.Equals, http.StatusForbidden) var perr params.Error err := json.Unmarshal(rr.Body.Bytes(), &perr) c.Assert(err, qt.IsNil) c.Assert(perr, qt.DeepEquals, params.Error{ Code: params.ErrForbidden, Message: "test error", }) } func (s *idpSuite) TestLoginFailureWithWait(c *qt.C) { id := "test" err := s.meetingPlace.NewRendezvous(context.Background(), id, []byte("test")) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() s.vc.Failure(context.Background(), rr, nil, id, errgo.WithCausef(nil, params.ErrForbidden, "test error")) c.Assert(rr.Code, qt.Equals, http.StatusForbidden) var perr params.Error err = json.Unmarshal(rr.Body.Bytes(), &perr) c.Assert(err, qt.IsNil) c.Assert(perr, qt.DeepEquals, params.Error{ Code: params.ErrForbidden, Message: "test error", }) ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() d1, d2, err := s.meetingPlace.Wait(ctx, id) c.Assert(err, qt.IsNil) c.Assert(string(d1), qt.Equals, "test") var li discharger.LoginInfo err = json.Unmarshal(d2, &li) c.Assert(err, qt.IsNil) c.Assert(li.ProviderID, qt.Equals, store.ProviderIdentity("")) c.Assert(li.Error.Message, qt.Equals, "test error") } func (s *idpSuite) TestLoginSuccess(c *qt.C) { req, err := http.NewRequest("GET", "", nil) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() s.vc.Success(context.Background(), rr, req, "", &store.Identity{ Username: "test-user", }) c.Assert(rr.Code, qt.Equals, http.StatusOK) c.Assert(rr.HeaderMap.Get("Content-Type"), qt.Equals, "text/plain; charset=utf-8") c.Assert(rr.Body.String(), qt.Equals, "Login successful as test-user") } func (s *idpSuite) TestLoginSuccessWithTemplate(c *qt.C) { _, err := s.template.New("login").Parse("

Login successful as {{.Username}}

") c.Assert(err, qt.IsNil) req, err := http.NewRequest("GET", "", nil) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() s.vc.Success(context.Background(), rr, req, "", &store.Identity{ Username: "test-user", }) c.Assert(rr.Code, qt.Equals, http.StatusOK) c.Assert(rr.HeaderMap.Get("Content-Type"), qt.Equals, "text/html;charset=utf-8") c.Assert(rr.Body.String(), qt.Equals, "

Login successful as test-user

") } func (s *idpSuite) TestLoginRedirectSuccess(c *qt.C) { req, err := http.NewRequest("GET", "", nil) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() s.vc.RedirectSuccess(context.Background(), rr, req, "http://example.com/callback", "1234", &store.Identity{ Username: "test-user", }) resp := rr.Result() body, err := ioutil.ReadAll(resp.Body) c.Assert(err, qt.IsNil) c.Assert(resp.StatusCode, qt.Equals, http.StatusSeeOther, qt.Commentf("%s", body)) loc, err := resp.Location() c.Assert(err, qt.IsNil) v := loc.Query() loc.RawQuery = "" c.Assert(loc.String(), qt.Equals, "http://example.com/callback") c.Assert(v.Get("state"), qt.Equals, "1234") c.Assert(v.Get("code"), qt.Not(qt.Equals), "") } func (s *idpSuite) TestLoginRedirectSuccessInvalidReturnTo(c *qt.C) { req, err := http.NewRequest("GET", "", nil) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() s.vc.RedirectSuccess(context.Background(), rr, req, "::", "1234", &store.Identity{ Username: "test-user", }) c.Assert(rr.Code, qt.Equals, http.StatusBadRequest) var perr params.Error err = json.Unmarshal(rr.Body.Bytes(), &perr) c.Assert(err, qt.IsNil) c.Assert(perr, qt.DeepEquals, params.Error{ Code: params.ErrBadRequest, Message: `invalid return_to "::": parse "::": missing protocol scheme`, }) } func (s *idpSuite) TestLoginRedirectSuccessReturnToNotTrusted(c *qt.C) { req, err := http.NewRequest("GET", "", nil) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() s.vc.RedirectSuccess(context.Background(), rr, req, "https://example.com", "1234", &store.Identity{ Username: "test-user", }) c.Assert(rr.Code, qt.Equals, http.StatusBadRequest) var perr params.Error err = json.Unmarshal(rr.Body.Bytes(), &perr) c.Assert(err, qt.IsNil) c.Assert(perr, qt.DeepEquals, params.Error{ Code: params.ErrBadRequest, Message: `invalid return_to "https://example.com"`, }) } func (s *idpSuite) TestLoginRedirectSuccessReturnToTrustedDomain(c *qt.C) { req, err := http.NewRequest("GET", "", nil) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() s.vc.RedirectSuccess(context.Background(), rr, req, "https://www.example.net/callback/path", "1234", &store.Identity{ Username: "test-user", }) resp := rr.Result() c.Assert(resp.StatusCode, qt.Equals, http.StatusSeeOther) loc, err := resp.Location() c.Assert(err, qt.IsNil) v := loc.Query() loc.RawQuery = "" c.Assert(loc.String(), qt.Equals, "https://www.example.net/callback/path") c.Assert(v.Get("state"), qt.Equals, "1234") c.Assert(v.Get("code"), qt.Not(qt.Equals), "") } func (s *idpSuite) TestLoginRedirectSuccessReturnToTrustedDomainWildcard(c *qt.C) { req, err := http.NewRequest("GET", "", nil) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() s.vc.RedirectSuccess(context.Background(), rr, req, "https://my.host.example.org/callback/path", "1234", &store.Identity{ Username: "test-user", }) resp := rr.Result() c.Assert(resp.StatusCode, qt.Equals, http.StatusSeeOther) loc, err := resp.Location() c.Assert(err, qt.IsNil) v := loc.Query() loc.RawQuery = "" c.Assert(loc.String(), qt.Equals, "https://my.host.example.org/callback/path") c.Assert(v.Get("state"), qt.Equals, "1234") c.Assert(v.Get("code"), qt.Not(qt.Equals), "") } func (s *idpSuite) TestLoginRedirectSuccessReturnToTrustedDomainInsecure(c *qt.C) { req, err := http.NewRequest("GET", "", nil) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() s.vc.RedirectSuccess(context.Background(), rr, req, "http://www.example.net/callback/path", "1234", &store.Identity{ Username: "test-user", }) c.Assert(rr.Code, qt.Equals, http.StatusBadRequest) var perr params.Error err = json.Unmarshal(rr.Body.Bytes(), &perr) c.Assert(err, qt.IsNil) c.Assert(perr, qt.DeepEquals, params.Error{ Code: params.ErrBadRequest, Message: `invalid return_to "http://www.example.net/callback/path"`, }) } func (s *idpSuite) TestLoginRedirectFailureInvalidReturnTo(c *qt.C) { req, err := http.NewRequest("GET", "", nil) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() s.vc.RedirectFailure(context.Background(), rr, req, "::", "1234", errgo.WithCausef(nil, params.ErrForbidden, "test error")) c.Assert(rr.Code, qt.Equals, http.StatusForbidden) var perr params.Error err = json.Unmarshal(rr.Body.Bytes(), &perr) c.Assert(err, qt.IsNil) c.Assert(perr, qt.DeepEquals, params.Error{ Code: params.ErrForbidden, Message: `test error`, }) } golang-github-canonical-candid-1.12.3/internal/discharger/internal/000077500000000000000000000000001457263123000252405ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/internal/discharger/internal/store.go000066400000000000000000000051161457263123000267260ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package internal import ( "context" "crypto/sha256" "encoding/base64" "encoding/json" "time" "github.com/juju/simplekv" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/store" ) // IdentityStore is a short-term store for identity information // associated with a specified key. It wraps a KeyValueStore. type IdentityStore struct { kvstore simplekv.Store store store.Store } // NewIdentityStore creates a new IdentityStore using the // given KeyValueStore for backing storage. func NewIdentityStore(kvstore simplekv.Store, store store.Store) *IdentityStore { return &IdentityStore{ kvstore: kvstore, store: store, } } // Put adds the given Identity to the store, returning the key that should // be used to later retrieve the identity. The Identity will // only be available in the store until the given expire time. func (s *IdentityStore) Put(ctx context.Context, id *store.Identity, expire time.Time) (string, error) { entry := providerIdentityEntry{ ProviderID: id.ProviderID, Expire: expire, } b, err := json.Marshal(entry) if err != nil { // This should be impossible. panic(err) } hash := sha256.Sum256(b) key := base64.RawURLEncoding.EncodeToString(hash[:]) if err := s.kvstore.Set(ctx, key, b, expire); err != nil { return "", errgo.Mask(err, errgo.Is(context.Canceled), errgo.Is(context.DeadlineExceeded)) } return key, nil } // Get retrieves the Identity with the given key from the store. If // there is no such token, or the token has expired, then the returned // error will have a cause of store.ErrNotFound. func (s *IdentityStore) Get(ctx context.Context, key string, id *store.Identity) error { b, err := s.kvstore.Get(ctx, key) if err != nil { if errgo.Cause(err) == simplekv.ErrNotFound { return errgo.WithCausef(err, store.ErrNotFound, "") } return errgo.Mask(err, errgo.Is(context.Canceled), errgo.Is(context.DeadlineExceeded)) } var entry providerIdentityEntry if err := json.Unmarshal(b, &entry); err != nil { return errgo.Mask(err) } if entry.Expire.Before(time.Now()) { return errgo.WithCausef(nil, store.ErrNotFound, "%q not found", key) } id.ProviderID = entry.ProviderID err = s.store.Identity(ctx, id) if errgo.Cause(err) == store.ErrNotFound { err = errgo.WithCausef(nil, store.ErrNotFound, "%q not found", key) } return errgo.Mask(err, errgo.Is(store.ErrNotFound), errgo.Is(context.Canceled), errgo.Is(context.DeadlineExceeded)) } type providerIdentityEntry struct { ProviderID store.ProviderIdentity Expire time.Time } golang-github-canonical-candid-1.12.3/internal/discharger/internal/store_test.go000066400000000000000000000156041457263123000277700ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package internal_test import ( "context" "testing" "time" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/google/go-cmp/cmp/cmpopts" "github.com/juju/simplekv" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/internal/discharger/internal" "github.com/canonical/candid/store" ) func TestStore(t *testing.T) { qtsuite.Run(qt.New(t), &storeSuite{}) } type storeSuite struct { store *candidtest.Store } func (s *storeSuite) Init(c *qt.C) { s.store = candidtest.NewStore() } func (s *storeSuite) TestRoundTrip(c *qt.C) { ctx := context.Background() kv, err := s.store.ProviderDataStore.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) st := internal.NewIdentityStore(kv, s.store.Store) id := store.Identity{ ProviderID: "test:test", Username: "test", Name: "Test User", Email: "test@example.com", } err = s.store.Store.UpdateIdentity(ctx, &id, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, }) c.Assert(err, qt.IsNil) key, err := st.Put(ctx, &id, time.Now().Add(time.Minute)) c.Assert(err, qt.IsNil) var id2 store.Identity err = st.Get(ctx, key, &id2) c.Assert(err, qt.IsNil) c.Check(id2, qt.CmpEquals(cmpopts.EquateEmpty()), id) } func (s *storeSuite) TestPutCanceled(c *qt.C) { ctx := context.Background() kv, err := s.store.ProviderDataStore.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) kv = withSet(kv, func(context.Context, string, []byte, time.Time) error { return context.Canceled }) st := internal.NewIdentityStore(kv, s.store.Store) id := store.Identity{ ProviderID: "test:test", Username: "test", Name: "Test User", Email: "test@example.com", } err = s.store.Store.UpdateIdentity(ctx, &id, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, }) c.Assert(err, qt.IsNil) _, err = st.Put(ctx, &id, time.Now().Add(time.Minute)) c.Assert(err, qt.ErrorMatches, "context canceled") c.Assert(errgo.Cause(err), qt.Equals, context.Canceled) } func (s *storeSuite) TestPutDeadlineExceeded(c *qt.C) { ctx := context.Background() kv, err := s.store.ProviderDataStore.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) kv = withSet(kv, func(context.Context, string, []byte, time.Time) error { return context.DeadlineExceeded }) st := internal.NewIdentityStore(kv, s.store.Store) id := store.Identity{ ProviderID: "test:test", Username: "test", Name: "Test User", Email: "test@example.com", } err = s.store.Store.UpdateIdentity(ctx, &id, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, }) c.Assert(err, qt.IsNil) _, err = st.Put(ctx, &id, time.Now().Add(time.Minute)) c.Assert(err, qt.ErrorMatches, "context deadline exceeded") c.Assert(errgo.Cause(err), qt.Equals, context.DeadlineExceeded) } func (s *storeSuite) TestGetNotFound(c *qt.C) { ctx := context.Background() kv, err := s.store.ProviderDataStore.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) kv = withGet(kv, func(context.Context, string) ([]byte, error) { return nil, simplekv.ErrNotFound }) st := internal.NewIdentityStore(kv, s.store.Store) err = st.Get(ctx, "", nil) c.Assert(err, qt.ErrorMatches, "not found") c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) } func (s *storeSuite) TestGetCanceled(c *qt.C) { ctx := context.Background() kv, err := s.store.ProviderDataStore.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) kv = withGet(kv, func(context.Context, string) ([]byte, error) { return nil, context.Canceled }) st := internal.NewIdentityStore(kv, s.store.Store) err = st.Get(ctx, "", nil) c.Assert(err, qt.ErrorMatches, "context canceled") c.Assert(errgo.Cause(err), qt.Equals, context.Canceled) } func (s *storeSuite) TestGetDeadlineExceeded(c *qt.C) { ctx := context.Background() kv, err := s.store.ProviderDataStore.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) kv = withGet(kv, func(context.Context, string) ([]byte, error) { return nil, context.DeadlineExceeded }) st := internal.NewIdentityStore(kv, s.store.Store) err = st.Get(ctx, "", nil) c.Assert(err, qt.ErrorMatches, "context deadline exceeded") c.Assert(errgo.Cause(err), qt.Equals, context.DeadlineExceeded) } func (s *storeSuite) TestGetInvalidJSON(c *qt.C) { ctx := context.Background() kv, err := s.store.ProviderDataStore.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) kv = withGet(kv, func(context.Context, string) ([]byte, error) { return []byte("}"), nil }) st := internal.NewIdentityStore(kv, s.store.Store) err = st.Get(ctx, "", nil) c.Assert(err, qt.ErrorMatches, "invalid character '}' looking for beginning of value") } func (s *storeSuite) TestExpiredEntry(c *qt.C) { ctx := context.Background() kv, err := s.store.ProviderDataStore.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) st := internal.NewIdentityStore(kv, s.store.Store) id := store.Identity{ ProviderID: "test:test", Username: "test", Name: "Test User", Email: "test@example.com", } err = s.store.Store.UpdateIdentity(ctx, &id, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, }) c.Assert(err, qt.IsNil) key, err := st.Put(ctx, &id, time.Now()) c.Assert(err, qt.IsNil) err = st.Get(ctx, key, nil) c.Assert(err, qt.ErrorMatches, `".*" not found`) c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) } func (s *storeSuite) TestIdentityNotInStore(c *qt.C) { ctx := context.Background() kv, err := s.store.ProviderDataStore.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) st := internal.NewIdentityStore(kv, s.store.Store) id := store.Identity{ ProviderID: "test:test", Username: "test", Name: "Test User", Email: "test@example.com", } key, err := st.Put(ctx, &id, time.Now().Add(time.Minute)) c.Assert(err, qt.IsNil) var id2 store.Identity err = st.Get(ctx, key, &id2) c.Assert(err, qt.ErrorMatches, `".*" not found`) c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) } type testGetStore struct { simplekv.Store f func(context.Context, string) ([]byte, error) } func (s testGetStore) Get(ctx context.Context, key string) ([]byte, error) { return s.f(ctx, key) } func withGet(store simplekv.Store, get func(context.Context, string) ([]byte, error)) simplekv.Store { return testGetStore{ Store: store, f: get, } } type testSetStore struct { simplekv.Store f func(context.Context, string, []byte, time.Time) error } func (s testSetStore) Set(ctx context.Context, key string, value []byte, expire time.Time) error { return s.f(ctx, key, value, expire) } func withSet(store simplekv.Store, set func(context.Context, string, []byte, time.Time) error) simplekv.Store { return testSetStore{ Store: store, f: set, } } golang-github-canonical-candid-1.12.3/internal/discharger/login.go000066400000000000000000000235411457263123000250700ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package discharger import ( "fmt" "net/http" "net/url" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/canonical/candid/idp/idputil" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) // legacyLoginRequest is a request to start a login to the identity manager // using the legacy visit-wait protocol. type legacyLoginRequest struct { httprequest.Route `httprequest:"GET /login-legacy"` Domain string `httprequest:"domain,form"` DischargeID string `httprequest:"did,form"` } // LoginLegacy handles the GET /login-legacy endpoint that is used to log in to Candid // when the legacy visit-wait protocol is used. func (h *handler) LoginLegacy(p httprequest.Params, req *legacyLoginRequest) error { // We should really be parsing the accept header properly here, but // it's really complicated http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 // perhaps use http://godoc.org/bitbucket.org/ww/goautoneg for this. // Probably not worth it now that it's only part of the legacy protocol. if p.Request.Header.Get("Accept") == "application/json" { methods := map[string]string{"agent": legacyAgentURL(h.params.Location, req.DischargeID)} for _, idp := range h.params.IdentityProviders { methods[idp.Name()] = idp.URL(req.DischargeID) } err := httprequest.WriteJSON(p.Response, http.StatusOK, methods) if err != nil { return errgo.Notef(err, "cannot write login methods") } return nil } _, _, err := agent.LoginCookie(p.Request) if errgo.Cause(err) != agent.ErrNoAgentLoginCookie { resp, err := h.LegacyAgentLogin(p, &legacyAgentLoginRequest{ DischargeID: req.DischargeID, }) if err != nil { return errgo.Mask(err, errgo.Any) } return httprequest.WriteJSON(p.Response, http.StatusOK, resp) } if err != nil && errgo.Cause(err) != agent.ErrNoAgentLoginCookie { return errgo.Notef(err, "bad agent-login cookie") } return h.Login(p, (*loginRequest)(req)) } // loginRequest is a request to start a login to the identity manager. type loginRequest struct { httprequest.Route `httprequest:"GET /login"` Domain string `httprequest:"domain,form"` DischargeID string `httprequest:"did,form"` } // Login handles the GET /v1/login endpoint that is used to log in to Candid. // when an interactive visit-wait protocol has been chosen by the client. func (h *handler) Login(p httprequest.Params, req *loginRequest) error { // Store the requested discharge ID in a session cookie so that // when the redirect comes back to login-complete we know the // login was initiated in this session. cookiePath := idputil.CookiePathRelativeToLocation("/login-complete", h.params.Location, h.params.SkipLocationForCookiePaths) state, err := h.params.codec.SetCookie(p.Response, waitCookieName, cookiePath, waitState{ DischargeID: req.DischargeID, }) if err != nil { return errgo.Mask(err) } v := url.Values{ "state": {state}, "return_to": {h.params.Location + "/login-complete"}, } if req.Domain != "" { v.Set("domain", req.Domain) } http.Redirect(p.Response, p.Request, h.params.Location+"/login-redirect?"+v.Encode(), http.StatusTemporaryRedirect) return nil } // redirectLoginRequest is a request to start a redirect based login to // the identity server. type redirectLoginRequest struct { httprequest.Route `httprequest:"GET /login-redirect"` // Domain holdes the requested identity provider domain, if any. Domain string `httprequest:"domain,form"` // ReturnTo holds the URL that the service will redirect to when // the login attempt is complete. ReturnTo string `httprequest:"return_to,form"` // State holds an opaque token that will be sent back to the // requesting service so the service can check that it initiated // the original login request. State string `httprequest:"state,form"` } // RedirectLogin handles starting a redirect based login request for a // domain (if specified). It produces a page with the possible choices of // identity provider which the user must then choose to start the login // process. func (h *handler) RedirectLogin(p httprequest.Params, req *redirectLoginRequest) error { cookiePath := idputil.CookiePathRelativeToLocation(idputil.LoginCookiePath, h.params.Location, h.params.SkipLocationForCookiePaths) state, err := h.params.codec.SetCookie(p.Response, idputil.LoginCookieName, cookiePath, idputil.LoginState{ ReturnTo: req.ReturnTo, State: req.State, Expires: time.Now().Add(15 * time.Minute), }) if err != nil { return errgo.Mask(err) } return errgo.Mask(h.authChoice(p.Response, p.Request, state, req.Domain, "", false)) } func (h *handler) authChoice(w http.ResponseWriter, req *http.Request, state, domain, errorMessage string, useEmail bool) error { // Find all the possible login methods. var allIDPs []params.IDPChoiceDetails var idps []params.IDPChoiceDetails for _, idp := range h.params.IdentityProviders { if !idp.Interactive() { continue } choice := params.IDPChoiceDetails{ Name: idp.Name(), Domain: idp.Domain(), Description: idp.Description(), Icon: idp.IconURL(), URL: idp.URL(state), } if !idp.Hidden() { allIDPs = append(allIDPs, choice) } if domain != "" && idp.Domain() == domain { idps = append(idps, choice) } } if len(allIDPs) == 0 { return errgo.Newf("no interactive login methods found") } if len(idps) == 0 { idps = allIDPs } if req.Header.Get("Accept") == "application/json" { httprequest.WriteJSON(w, http.StatusOK, params.IDPChoice{IDPs: idps}) return nil } type authenticationRequiredParams struct { params.TemplateBrandParameters IDPs []params.IDPChoiceDetails Error string UseEmail bool ShowEmailLink bool WithEmailURL string } authParams := authenticationRequiredParams{ TemplateBrandParameters: params.BrandParameters(), IDPs: idps, Error: errorMessage, UseEmail: useEmail, ShowEmailLink: h.params.EnableEmailLogin && domain == "" && !useEmail, WithEmailURL: h.params.Location + "/login-email?state=" + state, } if err := h.params.Template.ExecuteTemplate(w, "authentication-required", authParams); err != nil { return errgo.Mask(err) } return nil } type emailLoginRequest struct { httprequest.Route `httprequest:"GET /login-email"` // State holds the state information generated in the /login-redirect // handler. State string `httprequest:"state,form"` } // EmailLogin starts a request to choose an identity provider using an // email address. func (h *handler) EmailLogin(p httprequest.Params, req *emailLoginRequest) error { return h.authChoice(p.Response, p.Request, req.State, "", "", true) } type emailLoginSubmitRequest struct { httprequest.Route `httprequest:"POST /login-email"` // State holds the state information generated in the /login-redirect // handler. State string `httprequest:"state,form"` // Email holds the email address to use to select the identity // provider. Email string `httprequest:"email,form"` } // EmailLoginSubmit attemtps to find an identity provider suitable for // use with the provided email address and either redirects to that // identity provider, or outputs the same email form with an error message. func (h *handler) EmailLoginSubmit(p httprequest.Params, req *emailLoginSubmitRequest) error { for _, idp := range h.params.IdentityProviders { type isForEmailAddrer interface { IsForEmailAddr(string) bool } matcher, ok := idp.(isForEmailAddrer) if !ok { continue } if matcher.IsForEmailAddr(req.Email) { http.Redirect(p.Response, p.Request, idp.URL(req.State), http.StatusSeeOther) return nil } } return h.authChoice(p.Response, p.Request, req.State, "", fmt.Sprintf(`cannot find identity provider for email address "%s"`, req.Email), true) } // loginCompleteRequest is a request that completes a login attempt. type loginCompleteRequest struct { httprequest.Route `httprequest:"GET /login-complete"` // State holds the login state that was sent with the original // login request. This must match the candid-discharge-wait // cookie for the request to be processed. State string `httprequest:"state,form"` // Code holds the authorisation code to swap for the discharge // token. This is only set on successful requests. Code string `httprequest:"code,form"` // ErrorCode contains the error code, if any, for a failed login. ErrorCode string `httprequest:"error_code,form"` // Error holds the error message from a failed login. Error string `httprequest:"error,form"` } // LoginComplete handles completing the login process for visitor based // login flows. The redirect based login will return here with either a // code to get a discharge token, or an error. This endpoint completes // any waiting endpoint. func (h *handler) LoginComplete(p httprequest.Params, req *loginCompleteRequest) { ctx := p.Context var ws waitState if err := h.params.codec.Cookie(p.Request, waitCookieName, req.State, &ws); err != nil { logger.Infof("login error: %s", err) idputil.BadRequestf(p.Response, "invalid login state") return } if req.Error != "" { err := ¶ms.Error{ Message: req.Error, Code: params.ErrorCode(req.ErrorCode), } h.params.visitCompleter.Failure(ctx, p.Response, p.Request, ws.DischargeID, err) return } var id store.Identity if err := h.params.identityStore.Get(ctx, req.Code, &id); err != nil { h.params.visitCompleter.Failure(ctx, p.Response, p.Request, ws.DischargeID, err) return } h.params.visitCompleter.Success(ctx, p.Response, p.Request, ws.DischargeID, &id) } const waitCookieName = "candid-discharge-wait" // A waitState is a cookie that stores the current state of a login that // is part of a interact/wait pair. type waitState struct { DischargeID string } golang-github-canonical-candid-1.12.3/internal/discharger/login_sublocation_test.go000066400000000000000000000036171457263123000305330ustar00rootroot00000000000000// Copyright 2021 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package discharger_test import ( "net/http" "path/filepath" "testing" qt "github.com/frankban/quicktest" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/static" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/internal/discharger" "github.com/canonical/candid/internal/identity" ) const sublocationPath = "/sublocation" var cookiePathTests = []struct { about string skipLocation bool }{{ about: "location in the cookie", skipLocation: false, }, { about: "location NOT in the cookie", skipLocation: true, }} func TestLoginCookiePath(t *testing.T) { c := qt.New(t) for _, test := range cookiePathTests { c.Run(test.about, func(c *qt.C) { // Set up the store and the server. store := candidtest.NewStore() p := store.ServerParams() p.IdentityProviders = []idp.IdentityProvider{ static.NewIdentityProvider(static.Params{ Name: "test", Domain: "test", Icon: "/static/static1.bmp", }), } p.SkipLocationForCookiePaths = test.skipLocation srv := candidtest.NewServerWithSublocation(c, p, map[string]identity.NewAPIHandlerFunc{ "discharger": discharger.NewAPIHandler, }, sublocationPath) // Make the request. req, err := http.NewRequest("GET", sublocationPath+"/login", nil) c.Assert(err, qt.IsNil) req.Header.Set("Accept", "application/json") resp := srv.Do(c, req) defer resp.Body.Close() // Check the response. c.Assert(resp.StatusCode, qt.Equals, http.StatusOK) cookies := resp.Cookies() c.Assert(cookies, qt.Not(qt.HasLen), 0) for _, cookie := range cookies { dir := filepath.Dir(cookie.Path) if test.skipLocation { c.Assert(dir, qt.Not(qt.Equals), sublocationPath) } else { c.Assert(dir, qt.Equals, sublocationPath) } } }) } } golang-github-canonical-candid-1.12.3/internal/discharger/login_test.go000066400000000000000000000275311457263123000261320ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package discharger_test import ( "encoding/json" "html/template" "io/ioutil" "net/http" "net/url" "strings" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/static" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/internal/discharger" "github.com/canonical/candid/internal/identity" "github.com/canonical/candid/params" ) // loginTemplate contains the template to use in login tests. var loginTemplate *template.Template func init() { var err error loginTemplate, err = candidtest.DefaultTemplate.Clone() if err != nil { panic(err) } template.Must(loginTemplate.New("authentication-required").Parse(` {{range .IDPs}}{{.URL}} {{end}}Error: {{.Error}} UseEmail: {{.UseEmail}} ShowEmailLink: {{.ShowEmailLink}} WithEmailURL: {{.WithEmailURL}} `[1:])) } func TestLogin(t *testing.T) { qtsuite.Run(qt.New(t), &loginSuite{}) } type loginSuite struct { store *candidtest.Store srv *candidtest.Server dischargeCreator *candidtest.DischargeCreator interactor httpbakery.WebBrowserInteractor } func (s *loginSuite) Init(c *qt.C) { s.store = candidtest.NewStore() sp := s.store.ServerParams() sp.RedirectLoginTrustedURLs = []string{ "https://example.com/callback", } sp.IdentityProviders = []idp.IdentityProvider{ static.NewIdentityProvider(static.Params{ Name: "test", Users: map[string]static.UserInfo{ "test": { Password: "testpassword", Name: "Test User", Email: "test@example.com", Groups: []string{"test1", "test2"}, }, }, Icon: "/static/static1.bmp", }), static.NewIdentityProvider(static.Params{ Name: "test2", Domain: "test2", Icon: "/static/static2.bmp", MatchEmailAddr: "@example.com$", }), static.NewIdentityProvider(static.Params{ Name: "test3", Domain: "test3", Icon: "/static/static3.bmp", Hidden: true, }), } sp.Template = loginTemplate s.srv = candidtest.NewServer(c, sp, map[string]identity.NewAPIHandlerFunc{ "discharger": discharger.NewAPIHandler, }) s.dischargeCreator = candidtest.NewDischargeCreator(s.srv) s.interactor = httpbakery.WebBrowserInteractor{ OpenWebBrowser: candidtest.PasswordLogin(c, "test", "testpassword"), } } func (s *loginSuite) TestLegacyInteractiveLogin(c *qt.C) { client := s.srv.Client(s.interactor) // Use " 0, nil } func (a *Authenticator) returnError(w http.ResponseWriter, err error) { perr, ok := err.(*params.Error) if !ok { httprequest.WriteJSON(w, http.StatusInternalServerError, err) } status := http.StatusOK switch perr.Code { case params.ErrBadRequest: status = http.StatusBadRequest case params.ErrForbidden: status = http.StatusForbidden case params.ErrInternalServer: status = http.StatusInternalServerError } httprequest.WriteJSON( w, status, err, ) } // Handle servers incoming http requests. func (a *Authenticator) Handle(ctx context.Context, w http.ResponseWriter, req *http.Request) { switch strings.TrimPrefix(req.URL.Path, a.Params.URLPrefix) { case "/login": switch req.Method { case "POST": a.login(ctx, w, req) case "GET": var loginState idputil.LoginState if err := a.Params.Codec.Cookie(req, idputil.LoginCookieName, req.Form.Get("state"), &loginState); err != nil { idputil.BadRequestf(w, "login failed: invalid login state") return } id, err := a.verifyLogin(ctx, req) if err == nil { a.Params.VisitCompleter.RedirectSuccess(ctx, w, req, loginState.ReturnTo, loginState.State, id) } if errgo.Cause(err) == errNoValidCredentials { data, err := a.prepareFormData(ctx, w, req) if err != nil { a.Params.VisitCompleter.RedirectFailure(ctx, w, req, loginState.ReturnTo, loginState.State, errgo.Notef(err, "failed to prepare mfa form")) } err = a.Params.Template.ExecuteTemplate(w, "mfa", data) if err != nil { a.Params.VisitCompleter.RedirectFailure(ctx, w, req, loginState.ReturnTo, loginState.State, err) } return } a.Params.VisitCompleter.RedirectFailure(ctx, w, req, loginState.ReturnTo, loginState.State, errgo.New("mfa credentials not presented")) return } case "/remove": a.removeCredential(ctx, w, req) case "/remove-complete": a.removeCredentialComplete(ctx, w, req) case "/register": a.credentialRegistration(ctx, w, req) case "/manage": a.manage(ctx, w, req) } } // webAuthnUser fetches the identity with the specified providerID // and its credentials and returns a type that implements the webauthn.User // interface. func (a *Authenticator) webAuthnUser(ctx context.Context, providerID string) (*webauthnUser, error) { id := store.Identity{ ProviderID: store.ProviderIdentity(providerID), } // we fetch the identity to fill in the username and name // fields of the user. err := a.Params.Store.Identity(ctx, &id) if err != nil { return nil, errgo.Mask(err) } credentials, err := a.Params.Store.UserMFACredentials(ctx, string(id.ProviderID)) if err != nil { return nil, errgo.Mask(err) } return &webauthnUser{ Identity: &id, Credentials: credentials, }, nil } func (a *Authenticator) userCredentials(req *http.Request, user *webauthnUser) []formCredentialParams { creds := make([]formCredentialParams, len(user.Credentials)) for i, cred := range user.Credentials { v := url.Values{ "credential-name": {cred.Name}, } creds[i] = formCredentialParams{ Name: cred.Name, RemoveURL: a.Params.URLPrefix + "/remove?" + v.Encode(), } } return creds } // registrationData returns the data needed to register a new // credential. func (a *Authenticator) registrationData(ctx context.Context, user webauthn.User) (string, string, error) { credentialCreation, registrationSessionData, err := a.Authenticator.BeginRegistration( user, webauthn.WithAuthenticatorSelection( protocol.AuthenticatorSelection{ RequireResidentKey: protocol.ResidentKeyUnrequired(), UserVerification: protocol.VerificationDiscouraged, }), webauthn.WithConveyancePreference(protocol.PreferNoAttestation), ) if err != nil { return "", "", errgo.Mask(err) } credentialCreationData, err := json.Marshal(credentialCreation) if err != nil { return "", "", errgo.Mask(err) } sessionData, err := json.Marshal(registrationSessionData) if err != nil { return "", "", errgo.Mask(err) } return string(credentialCreationData), string(sessionData), nil } // loginData returns the data needed to verify any existing // user credential. func (a *Authenticator) loginData(ctx context.Context, user webauthn.User) (string, string, error) { loginOptions, loginSessionData, err := a.Authenticator.BeginLogin( user, webauthn.WithUserVerification(protocol.VerificationDiscouraged), ) if err != nil { return "", "", errgo.Mask(err) } loginData, err := json.Marshal(loginOptions) if err != nil { return "", "", errgo.Mask(err) } sessionData, err := json.Marshal(loginSessionData) if err != nil { return "", "", errgo.Mask(err) } return string(loginData), string(sessionData), nil } // prepareFormData presents data needed to present a form to the user // enabling the user to either register a new security device or login using // an existing device. func (a *Authenticator) prepareFormData(ctx context.Context, w http.ResponseWriter, req *http.Request) (*formParams, error) { var state LoginState if err := a.Params.Codec.Cookie(req, CookieName, req.Form.Get(StateName), &state); err != nil { return nil, errgo.Mask(err) } user, err := a.webAuthnUser(ctx, state.ProviderID) if err != nil { if errgo.Cause(err) == store.ErrNotFound { return nil, errgo.WithCausef(nil, params.ErrForbidden, "forbidden") } return nil, errgo.Mask(err) } data := formParams{ RegistrationURL: idputil.RedirectURL(a.Params.URLPrefix, "/register", req.Form.Get("state")), LoginURL: idputil.RedirectURL(a.Params.URLPrefix, "/login", req.Form.Get("state")), Credentials: a.userCredentials(req, user), } registrationData, registrationSessionData, err := a.registrationData(ctx, user) if err != nil { return nil, errgo.Mask(err) } state.RegistrationSessionData = registrationSessionData data.RegistrationData = registrationData // if the user already has registered security devices, we // enable login using any of them if len(user.WebAuthnCredentials()) > 0 { loginData, sessionData, err := a.loginData(ctx, user) if err != nil { return nil, errgo.Mask(err) } state.LoginSessionData = sessionData data.LoginData = loginData } else { data.LoginData = "{}" data.MustRegister = true } cookiePath := idputil.CookiePathRelativeToLocation(CookiePath, a.Params.Location, a.Params.SkipLocationForCookiePaths) mfaState, err := a.Params.Codec.SetCookie(w, CookieName, cookiePath, state) if err != nil { return nil, errgo.Mask(err) } data.MFAState = mfaState if len(user.Credentials) == 0 { data.Note = "Identity provider requires MFA. Please register a security key." } data.TemplateBrandParameters = params.BrandParameters() return &data, nil } // credentialRegistration method is used to finish the mfa security device registration. func (a *Authenticator) credentialRegistration(ctx context.Context, w http.ResponseWriter, req *http.Request) { // get the credential name from the request credentialName := req.Form.Get("credential-name") if credentialName == "" { a.returnError(w, params.NewError(params.ErrBadRequest, "credential name not specified")) } // get the login state var state LoginState if err := a.Params.Codec.Cookie(req, CookieName, req.Form.Get(StateName), &state); err != nil { a.returnError(w, params.NewError(params.ErrBadRequest, "invalid mfa login state")) return } // get the user specified in the login state user, err := a.webAuthnUser(ctx, state.ProviderID) if err != nil { if errgo.Cause(err) == store.ErrNotFound { a.returnError(w, params.NewError(params.ErrForbidden, "forbidden")) return } a.returnError(w, params.NewError(params.ErrInternalServer, "internal server error")) return } // unmarshal the registration session data var sessionData webauthn.SessionData err = json.Unmarshal([]byte(state.RegistrationSessionData), &sessionData) if err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, "invalid session data")) return } // verify the response credential, err := a.Authenticator.FinishRegistration(user, sessionData, req) if err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } // add the user's mfa credential to the store err = a.Params.Store.AddMFACredential(ctx, store.MFACredential{ ProviderID: user.Identity.ProviderID, Name: credentialName, ID: credential.ID, PublicKey: credential.PublicKey, AttestationType: credential.AttestationType, AuthenticatorGUID: credential.Authenticator.AAGUID, AuthenticatorSignCount: credential.Authenticator.SignCount, }) if err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } // refresh user credentials err = user.refreshCredentials(ctx, a.Params.Store) if err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } // set the newly registered credentials as the currently valid credential // for this user state.ValidCredentialID = credential.ID registrationData, registrationSessionData, err := a.registrationData(ctx, user) if err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } state.RegistrationSessionData = registrationSessionData cookiePath := idputil.CookiePathRelativeToLocation(CookiePath, a.Params.Location, a.Params.SkipLocationForCookiePaths) mfaState, err := a.Params.Codec.SetCookie(w, CookieName, cookiePath, state) if err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } // respond with the updated state and user credentials creds := a.userCredentials(req, user) data := struct { State string `json:"state"` Credentials []formCredentialParams `json:"credentials"` RegistrationData string `json:"registrationdata"` }{ State: mfaState, Credentials: creds, RegistrationData: registrationData, } httprequest.WriteJSON(w, http.StatusOK, data) } // verifyLogin returns an error if the user has not yet presented valid mfa credentials. func (a *Authenticator) verifyLogin(ctx context.Context, req *http.Request) (*store.Identity, error) { // get the login state var state LoginState if err := a.Params.Codec.Cookie(req, CookieName, req.Form.Get(StateName), &state); err != nil { return nil, errgo.New("login state not found") } // verify that the user has previously presented valid credentials if len(state.ValidCredentialID) == 0 { return nil, errNoValidCredentials } // fetch and return the user's identity id := store.Identity{ ProviderID: store.ProviderIdentity(state.ProviderID), } err := a.Params.Store.Identity(ctx, &id) if err != nil { return nil, errgo.Mask(err) } return &id, nil } // login method is used to complete the mfa part of the login process. func (a *Authenticator) login(ctx context.Context, w http.ResponseWriter, req *http.Request) { // get the login state var state LoginState if err := a.Params.Codec.Cookie(req, CookieName, req.Form.Get(StateName), &state); err != nil { a.returnError(w, params.NewError(params.ErrBadRequest, "invalid mfa login state")) return } // get the user specified in the login state user, err := a.webAuthnUser(ctx, state.ProviderID) if err != nil { if errgo.Cause(err) == store.ErrNotFound { a.returnError(w, params.NewError(params.ErrForbidden, "forbidden")) return } a.returnError(w, params.NewError(params.ErrInternalServer, "internal server error")) return } // unmarshal the login session data var sessionData webauthn.SessionData err = json.Unmarshal([]byte(state.LoginSessionData), &sessionData) if err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } // validate presented credentials validCredential, err := a.Authenticator.FinishLogin(user, sessionData, req) if err != nil { a.returnError(w, params.NewError(params.ErrForbidden, err.Error())) return } // update authenticator sig count err = a.Params.Store.IncrementMFACredentialSignCount(ctx, validCredential.ID) if err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } // set the presented credenitals as currently valid credentials for the user state.ValidCredentialID = validCredential.ID registrationData, registrationSessionData, err := a.registrationData(ctx, user) if err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } state.RegistrationSessionData = registrationSessionData cookiePath := idputil.CookiePathRelativeToLocation(CookiePath, a.Params.Location, a.Params.SkipLocationForCookiePaths) mfaState, err := a.Params.Codec.SetCookie(w, CookieName, cookiePath, state) if err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } // respond with the new state, user credentials, and data // that can be used to register new credentials creds := a.userCredentials(req, user) data := struct { State string `json:"state"` Credentials []formCredentialParams `json:"credentials"` RegistrationData string `json:"registrationdata"` }{ State: mfaState, Credentials: creds, RegistrationData: registrationData, } httprequest.WriteJSON(w, http.StatusOK, data) } type removeCredentialParams struct { params.TemplateBrandParameters Name string MFAState string RemoveURL string ManageURL string } // removeCredential renders the removal confirmation template. func (a *Authenticator) removeCredential(ctx context.Context, w http.ResponseWriter, req *http.Request) { // get the credential name from the request credentialName := req.Form.Get("credential-name") v := url.Values{ "credential-name": []string{credentialName}, } data := removeCredentialParams{ TemplateBrandParameters: params.BrandParameters(), MFAState: req.Form.Get(StateName), Name: credentialName, RemoveURL: a.Params.URLPrefix + "/remove-complete?" + v.Encode(), ManageURL: a.Params.URLPrefix + "/manage", } err := a.Params.Template.ExecuteTemplate(w, "remove-credential-confirmation", data) if err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } } // removeCredentialComplete removes the user's mfa security device. func (a *Authenticator) removeCredentialComplete(ctx context.Context, w http.ResponseWriter, req *http.Request) { // get the login state var state LoginState if err := a.Params.Codec.Cookie(req, CookieName, req.Form.Get(StateName), &state); err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } // get the credential name from the request credentialName := req.Form.Get("credential-name") // fetch the user specified in the login state user, err := a.webAuthnUser(ctx, state.ProviderID) if err != nil { if errgo.Cause(err) == store.ErrNotFound { a.returnError(w, params.NewError(params.ErrForbidden, "forbidden")) return } a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } // remove credentials err = a.Params.Store.RemoveMFACredential(ctx, state.ProviderID, credentialName) if err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } err = user.refreshCredentials(ctx, a.Params.Store) if err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } creds := a.userCredentials(req, user) data := struct { Credentials []formCredentialParams `json:"credentials"` }{ Credentials: creds, } httprequest.WriteJSON(w, http.StatusOK, data) } func (a *Authenticator) manage(ctx context.Context, w http.ResponseWriter, req *http.Request) { data, err := a.prepareFormData(ctx, w, req) if err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } err = a.Params.Template.ExecuteTemplate(w, "mfa-manage", data) if err != nil { a.returnError(w, params.NewError(params.ErrInternalServer, err.Error())) return } } golang-github-canonical-candid-1.12.3/internal/monitoring/000077500000000000000000000000001457263123000234765ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/internal/monitoring/meeting.go000066400000000000000000000025431457263123000254610ustar00rootroot00000000000000package monitoring import ( "time" "github.com/prometheus/client_golang/prometheus" ) type MeetingMetrics struct { meetingCompleted prometheus.Summary meetingsExpired prometheus.Counter } func NewMeetingMetrics() *MeetingMetrics { meetingCompleted := prometheus.NewSummary(prometheus.SummaryOpts{ Namespace: "candid", Subsystem: "rendevous", Name: "meetings_completed_times", Help: "The time between rendevous creation and its completion.", }) mustRegisterPrometheusCollector(meetingCompleted) meetingsExpired := prometheus.NewCounter(prometheus.CounterOpts{ Namespace: "candid", Subsystem: "rendevous", Name: "meetings_expired_count", Help: "Count of rendevous which were never completed.", }) mustRegisterPrometheusCollector(meetingsExpired) return &MeetingMetrics{ meetingCompleted: meetingCompleted, meetingsExpired: meetingsExpired, } } func mustRegisterPrometheusCollector(c prometheus.Collector) { err := prometheus.DefaultRegisterer.Register(c) if err == nil { return } if _, ok := err.(prometheus.AlreadyRegisteredError); ok { return } panic(err) } func (m *MeetingMetrics) RequestCompleted(startTime time.Time) { m.meetingCompleted.Observe(float64(time.Since(startTime)) / float64(time.Microsecond)) } func (m *MeetingMetrics) RequestsExpired(count int) { m.meetingsExpired.Add(float64(count)) } golang-github-canonical-candid-1.12.3/internal/monitoring/request.go000066400000000000000000000015241457263123000255170ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package monitoring import ( "time" "github.com/prometheus/client_golang/prometheus" "gopkg.in/httprequest.v1" ) var ( requestDuration = prometheus.NewSummaryVec(prometheus.SummaryOpts{ Namespace: "candid", Subsystem: "handler", Name: "request_duration", Help: "The duration of a web request.", }, []string{"path_pattern"}) ) func init() { prometheus.MustRegister(requestDuration) } type Request struct { startTime time.Time params *httprequest.Params } func NewRequest(p *httprequest.Params) Request { return Request{ startTime: time.Now(), params: p, } } func (r Request) ObserveMetric() { requestDuration.WithLabelValues(r.params.PathPattern).Observe(float64(time.Since(r.startTime)) / float64(time.Second)) } golang-github-canonical-candid-1.12.3/internal/monitoring/store.go000066400000000000000000000020101457263123000251520ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package monitoring import ( "context" "github.com/juju/loggo" "github.com/prometheus/client_golang/prometheus" "github.com/canonical/candid/store" ) var logger = loggo.GetLogger("candid.internal.monitoring") type StoreCollector struct { Store store.Store } var storeIdentiesDesc = prometheus.NewDesc( "candid_store_identities", "Number of stored identities", []string{"provider"}, nil, ) // Describe implements prometheus.Collector func (c StoreCollector) Describe(ch chan<- *prometheus.Desc) { ch <- storeIdentiesDesc } // Describe implements prometheus.Collector func (c StoreCollector) Collect(ch chan<- prometheus.Metric) { counts, err := c.Store.IdentityCounts(context.Background()) if err != nil { logger.Infof("error collecting metrics: %s", err) return } for provider, count := range counts { ch <- prometheus.MustNewConstMetric(storeIdentiesDesc, prometheus.GaugeValue, float64(count), provider) } } golang-github-canonical-candid-1.12.3/internal/v1/000077500000000000000000000000001457263123000216375ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/internal/v1/api.go000066400000000000000000000053421457263123000227430ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package v1 import ( "context" "github.com/juju/loggo" "golang.org/x/net/trace" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/internal/auth/httpauth" "github.com/canonical/candid/internal/identity" "github.com/canonical/candid/internal/monitoring" "github.com/canonical/candid/params" ) var logger = loggo.GetLogger("candid.internal.v1") // NewAPIHandler is an identity.NewAPIHandlerFunc. func NewAPIHandler(params identity.HandlerParams) ([]httprequest.Handler, error) { return identity.ReqServer.Handlers(new(params)), nil } // new returns a function that will generate a new instance of the v1 API // handler for a request. func new(hParams identity.HandlerParams) func(p httprequest.Params, arg interface{}) (*handler, context.Context, error) { reqAuth := httpauth.New(hParams.Oven, hParams.Authorizer, hParams.APIMacaroonTimeout) return func(p httprequest.Params, arg interface{}) (*handler, context.Context, error) { t := trace.New("identity.internal.v1", p.PathPattern) ctx := trace.NewContext(p.Context, t) ctx, close1 := hParams.Store.Context(p.Context) ctx, close2 := hParams.MeetingStore.Context(ctx) hnd := &handler{ params: hParams, trace: t, monReq: monitoring.NewRequest(&p), close: func() { close2() close1() }, } op := opForRequest(arg) logger.Debugf("opForRequest %#v -> %#v", arg, op) if op.Entity == "" { hnd.Close() return nil, nil, params.ErrUnauthorized } authInfo, err := reqAuth.Auth(ctx, p.Request, op) if err != nil { hnd.Close() return nil, nil, errgo.Mask(err, errgo.Any) } if authInfo.Identity != nil { id, ok := authInfo.Identity.(*auth.Identity) if !ok { hnd.Close() return nil, nil, errgo.Newf("unexpected identity type %T", authInfo.Identity) } ctx = contextWithIdentity(ctx, id) } return hnd, ctx, nil } } // A handler is a handler for a request to a /v1 endpoint. type handler struct { params identity.HandlerParams trace trace.Trace monReq monitoring.Request close func() } // Close implements io.Closer. httprequest will automatically call this // once a request is complete. func (h *handler) Close() error { if h.close != nil { h.close() h.close = nil } h.monReq.ObserveMetric() if h.trace != nil { h.trace.Finish() h.trace = nil } return nil } type identityKey struct{} func contextWithIdentity(ctx context.Context, identity *auth.Identity) context.Context { return context.WithValue(ctx, identityKey{}, identity) } func identityFromContext(ctx context.Context) *auth.Identity { id, _ := ctx.Value(identityKey{}).(*auth.Identity) return id } golang-github-canonical-candid-1.12.3/internal/v1/auth.go000066400000000000000000000056341457263123000231370ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package v1 import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/params" ) // opForRequest returns the operation that will be performed // by the API handler method which takes the given argument r. // See aclForOp in ../auth/auth.go for the mapping from // operation to ACLs. func opForRequest(r interface{}) bakery.Op { switch r := r.(type) { case *params.QueryUsersRequest: if r.Owner != "" { return auth.UserOp(params.Username(r.Owner), auth.ActionRead) } return auth.GlobalOp(auth.ActionRead) case *params.UserRequest: return auth.UserOp(r.Username, auth.ActionRead) case *params.SetUserRequest: // TODO require special permissions if the user // expiry time is less than some threshold? if r.Owner != "" { return auth.UserOp(r.Owner, auth.ActionCreateAgent) } return auth.UserOp(r.Username, auth.ActionWriteAdmin) case *params.CreateAgentRequest: if r.Parent { return auth.GlobalOp(auth.ActionCreateParentAgent) } return auth.GlobalOp(auth.ActionCreateAgent) case *params.UserGroupsRequest: return auth.UserOp(r.Username, auth.ActionReadGroups) case *params.SetUserGroupsRequest: return auth.UserOp(r.Username, auth.ActionWriteGroups) case *params.ModifyUserGroupsRequest: return auth.UserOp(r.Username, auth.ActionWriteGroups) case *params.UserIDPGroupsRequest: return auth.UserOp(r.Username, auth.ActionReadGroups) case *params.WhoAmIRequest: return identchecker.LoginOp case *params.SSHKeysRequest: return auth.UserOp(r.Username, auth.ActionReadSSHKeys) case *params.PutSSHKeysRequest: return auth.UserOp(r.Username, auth.ActionWriteSSHKeys) case *params.DeleteSSHKeysRequest: return auth.UserOp(r.Username, auth.ActionWriteSSHKeys) case *params.UserTokenRequest: return auth.UserOp(r.Username, auth.ActionReadAdmin) case *params.VerifyTokenRequest: return auth.GlobalOp(auth.ActionVerify) case *params.UserExtraInfoRequest: return auth.UserOp(r.Username, auth.ActionReadAdmin) case *params.SetUserExtraInfoRequest: return auth.UserOp(r.Username, auth.ActionWriteAdmin) case *params.UserExtraInfoItemRequest: return auth.UserOp(r.Username, auth.ActionReadAdmin) case *params.SetUserExtraInfoItemRequest: return auth.UserOp(r.Username, auth.ActionWriteAdmin) case *params.DischargeTokenForUserRequest: return auth.GlobalOp(auth.ActionDischargeFor) case *params.GetUserWithIDRequest: return auth.UserIDOp(r.UserID, auth.ActionRead) case *params.GetUserGroupsWithIDRequest: return auth.UserIDOp(r.UserID, auth.ActionReadGroups) case *params.ClearUserMFACredentialsRequest: return auth.GlobalOp(auth.ActionClearUserMFACredentials) default: logger.Infof("unknown API argument type %#v", r) } return bakery.Op{} } golang-github-canonical-candid-1.12.3/internal/v1/export_test.go000066400000000000000000000002171457263123000245460ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package v1 var ( GravatarHash = gravatarHash ) golang-github-canonical-candid-1.12.3/internal/v1/gravatar_test.go000066400000000000000000000012401457263123000250310ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package v1_test import ( "testing" qt "github.com/frankban/quicktest" v1 "github.com/canonical/candid/internal/v1" ) func TestGravatarHash(t *testing.T) { c := qt.New(t) c.Assert(v1.GravatarHash("myemail@domain.com"), qt.Equals, v1.GravatarHash("myemail@domain.com ")) c.Assert(v1.GravatarHash("myemail@domain.com"), qt.Equals, v1.GravatarHash(" myemail@domain.com")) c.Assert(v1.GravatarHash("myemail@domain.com"), qt.Equals, v1.GravatarHash("MYEMAIL@domain.com")) c.Assert(v1.GravatarHash("jbloggs3@example.com"), qt.Equals, "21e89fe03e3a3cc553933f99eb442d94") } golang-github-canonical-candid-1.12.3/internal/v1/users.go000066400000000000000000000546471457263123000233470ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package v1 import ( "context" "crypto/md5" "crypto/rand" "encoding/json" "fmt" "strings" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" macaroon "gopkg.in/macaroon.v2" "github.com/canonical/candid/candidclient" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) var disallowedUsernames = map[params.Username]bool{ "admin": true, "everyone": true, auth.AdminUsername: true, } // QueryUsers filters the user database for users that match the given // request. If no filters are requested all usernames will be returned. func (h *handler) QueryUsers(p httprequest.Params, r *params.QueryUsersRequest) ([]string, error) { logger.Tracef("QueryUsers %#v", r) var identity store.Identity var filter store.Filter if r.ExternalID != "" { identity.ProviderID = store.ProviderIdentity(r.ExternalID) filter[store.ProviderID] = store.Equal } if r.Email != "" { identity.Email = r.Email filter[store.Email] = store.Equal } if len(r.LastLoginSince) > 0 { var t time.Time if err := t.UnmarshalText([]byte(r.LastLoginSince)); err != nil { return nil, errgo.Notef(err, "cannot unmarshal last-login-since") } identity.LastLogin = t filter[store.LastLogin] = store.GreaterThanOrEqual } if len(r.LastDischargeSince) > 0 { var t time.Time if err := t.UnmarshalText([]byte(r.LastDischargeSince)); err != nil { return nil, errgo.Notef(err, "cannot unmarshal last-discharge-since") } identity.LastDischarge = t filter[store.LastDischarge] = store.GreaterThanOrEqual } if r.Owner != "" { ownerIdentity := store.Identity{ Username: r.Owner, } err := h.params.Store.Identity(p.Context, &ownerIdentity) if errgo.Cause(err) == store.ErrNotFound { // If the owner doesn't exist then it has no agents. return []string{}, nil } if err != nil { return nil, errgo.Mask(err) } identity.Owner = ownerIdentity.ProviderID filter[store.Owner] = store.Equal } // TODO(mhilton) make sure this endpoint can be queried as a // subset once there are more users. identities, err := h.params.Store.FindIdentities(p.Context, &identity, filter, []store.Sort{{Field: store.Username}}, 0, 0) if err != nil { return nil, errgo.Mask(err) } usernames := make([]string, len(identities)) for i, id := range identities { usernames[i] = id.Username } logger.Tracef("QueryUsers response %#v", usernames) return usernames, nil } // ClearUserMFACredentials removes all MFA credentials for a user. func (h *handler) ClearUserMFACredentials(p httprequest.Params, r *params.ClearUserMFACredentialsRequest) error { logger.Tracef("User %#v", r) id, err := h.params.Authorizer.Identity(p.Context, &store.Identity{ Username: string(r.Username), }) if err != nil { return errgo.Mask(err) } err = h.params.Store.ClearMFACredentials(p.Context, string(id.ProviderID)) if err != nil { return errgo.Mask(err) } return nil } // User returns the user information for the request user. func (h *handler) User(p httprequest.Params, r *params.UserRequest) (*params.User, error) { logger.Tracef("User %#v", r) id, err := h.params.Authorizer.Identity(p.Context, &store.Identity{ Username: string(r.Username), }) if err != nil { return nil, errgo.Mask(err, errgo.Is(params.ErrNotFound)) } u, err := h.userFromIdentity(p.Context, id) if err != nil { return nil, errgo.Mask(err) } logger.Tracef("User response %#v", u) return u, nil } // CreateAgent creates a new agent and returns the newly chosen username // for the agent. func (h *handler) CreateAgent(p httprequest.Params, u *params.CreateAgentRequest) (*params.CreateAgentResponse, error) { logger.Tracef("CreateAgent %#v", u) ctx := p.Context pks, err := publicKeys(u.PublicKeys) if err != nil { return nil, errgo.WithCausef(err, params.ErrBadRequest, "") } if len(pks) == 0 { // TODO if a we an endpoint to push/pull public keys, we won't need // to require this any more, because it could be done afterwards // (by someone with permission). return nil, errgo.WithCausef(nil, params.ErrBadRequest, "no public keys specified") } ownerAuthIdentity := identityFromContext(ctx) if ownerAuthIdentity == nil { return nil, errgo.Newf("no identity found (should not happen)") } if err := checkAuthIdentityIsMemberOf(ctx, ownerAuthIdentity, u.Groups); err != nil { return nil, errgo.Mask(err, errgo.Is(params.ErrForbidden)) } owner := ownerAuthIdentity.Identity if owner.ProviderID.Provider() == "idm" && owner.Owner != "" && !u.Parent { // Agent users, that are not parent agents, are not // allowed to create their own agents. // TODO a nicer way to do this check might be to express // it as a group permission - all non-agent users are in // the "can create agents" group. // TODO In the future, we might allow agents to create // other agents, but we'll have to work out what to do // about hierarchy - if agent A creates agent B, then A // is removed from a group but its owner is still a // member of that group, should B still have access to // the group? return nil, errgo.Newf("cannot create an agent using an agent account") } agentName, err := newAgentName() if err != nil { return nil, errgo.Mask(err) } identity := &store.Identity{ Username: agentName + "@candid", ProviderID: store.MakeProviderIdentity("idm", agentName), Name: u.FullName, Groups: u.Groups, PublicKeys: pks, ProviderInfo: map[string][]string{ "creator": {string(owner.ProviderID)}, }, } update := store.Update{ store.Username: store.Set, store.PublicKeys: store.Set, store.Groups: store.Set, store.Name: store.Set, store.ProviderInfo: store.Set, } if !u.Parent { identity.Owner = owner.ProviderID update[store.Owner] = store.Set } // TODO add tags to Identity? if err := h.params.Store.UpdateIdentity(p.Context, identity, update); err != nil { return nil, translateStoreError(err) } resp := ¶ms.CreateAgentResponse{ Username: params.Username(identity.Username), } logger.Tracef("CreateAgent response %#v", resp) return resp, nil } // SetUserDeprecated creates or updates the user with the given username. If the // user already exists then any IDPGroups or SSHKeys specified in the // request will be ignored. See SetUserGroups, ModifyUserGroups, // SetSSHKeys and DeleteSSHKeys if you wish to manipulate these for a // user. // TODO change this into a create-agent function. func (h *handler) SetUserDeprecated(p httprequest.Params, u *params.SetUserRequest) error { return errgo.WithCausef(nil, params.ErrForbidden, "PUT to /u/:username is disabled - please use a newer version of the client") } // WhoAmI returns details of the authenticated user. func (h *handler) WhoAmI(p httprequest.Params, arg *params.WhoAmIRequest) (params.WhoAmIResponse, error) { logger.Tracef("WhoAmI") id := identityFromContext(p.Context) if id == nil || id.Id() == "" { // Should never happen, as the endpoint should require authentication. return params.WhoAmIResponse{}, errgo.Newf("no identity") } resp := params.WhoAmIResponse{ User: string(id.Id()), } logger.Tracef("WhoAmI response %#v", resp) return resp, nil } // UserGroups returns the list of groups associated with the requested // user. func (h *handler) UserGroups(p httprequest.Params, r *params.UserGroupsRequest) ([]string, error) { logger.Tracef("UserGroups %#v", r) id, err := h.params.Authorizer.Identity(p.Context, &store.Identity{ Username: string(r.Username), }) if err != nil { return nil, errgo.Mask(err, errgo.Is(params.ErrNotFound)) } groups, err := id.Groups(p.Context) if err != nil { return nil, errgo.Mask(err) } if groups == nil { groups = []string{} } logger.Tracef("UserGroups response %#v", groups) return groups, nil } // UserIDPGroups returns the list of groups associated with the requested // user. This is deprected and UserGroups should be used in preference. func (h *handler) UserIDPGroups(p httprequest.Params, r *params.UserIDPGroupsRequest) ([]string, error) { return h.UserGroups(p, ¶ms.UserGroupsRequest{ Username: r.Username, }) } // SetUserGroups updates the groups stored for the given user to the // given value. func (h *handler) SetUserGroups(p httprequest.Params, r *params.SetUserGroupsRequest) error { logger.Tracef("SetUserGroups %#v", r) identity := store.Identity{ Username: string(r.Username), Groups: r.Groups.Groups, } err := h.params.Store.UpdateIdentity(p.Context, &identity, store.Update{store.Groups: store.Set}) if err != nil { return translateStoreError(err) } logger.Tracef("SetUserGroups complete") return nil } // ModifyUserGroups updates the groups stored for the given user. Groups // can be either added or removed in a single query. It is an error to // try and both add and remove groups at the same time. func (h *handler) ModifyUserGroups(p httprequest.Params, r *params.ModifyUserGroupsRequest) error { logger.Tracef("ModifyUserGroups %#v", r) identity := store.Identity{ Username: string(r.Username), } var update store.Update if len(r.Groups.Add) > 0 && len(r.Groups.Remove) > 0 { return errgo.WithCausef(nil, params.ErrBadRequest, "cannot add and remove groups in the same operation") } if len(r.Groups.Add) > 0 { identity.Groups = r.Groups.Add update[store.Groups] = store.Push } else { identity.Groups = r.Groups.Remove update[store.Groups] = store.Pull } err := h.params.Store.UpdateIdentity(p.Context, &identity, update) if err != nil { return translateStoreError(err) } logger.Tracef("SetUserGroups complete") return nil } // GetSSHKeys returns any SSH keys stored for the given user. func (h *handler) GetSSHKeys(p httprequest.Params, r *params.SSHKeysRequest) (params.SSHKeysResponse, error) { logger.Tracef("GetSSHKeys %#v", r) id := store.Identity{ Username: string(r.Username), } if err := h.params.Store.Identity(p.Context, &id); err != nil { return params.SSHKeysResponse{}, translateStoreError(err) } resp := params.SSHKeysResponse{ SSHKeys: id.ExtraInfo["sshkeys"], } logger.Tracef("GetSSHKeys response %#v", resp) return resp, nil } // PutSSHKeys updates the set of SSH keys stored for the given user. If // the add parameter is set to true then keys that are already stored // will be added to, otherwise they will be replaced. func (h *handler) PutSSHKeys(p httprequest.Params, r *params.PutSSHKeysRequest) error { logger.Tracef("PutSSHKeys %#v", r) id := store.Identity{ Username: string(r.Username), ExtraInfo: map[string][]string{ "sshkeys": r.Body.SSHKeys, }, } update := store.Update{ store.ExtraInfo: store.Push, } err := h.params.Store.UpdateIdentity(p.Context, &id, update) if err != nil { return translateStoreError(err) } logger.Tracef("PutSSHKeys complete") return nil } // DeleteSSHKeys removes all of the ssh keys specified from the keys // stored for the given user. It is not an error to attempt to remove a // key that is not associated with the user. func (h *handler) DeleteSSHKeys(p httprequest.Params, r *params.DeleteSSHKeysRequest) error { logger.Tracef("DeleteSSHKeys %#v", r) id := store.Identity{ Username: string(r.Username), ExtraInfo: map[string][]string{ "sshkeys": r.Body.SSHKeys, }, } update := store.Update{ store.ExtraInfo: store.Pull, } err := h.params.Store.UpdateIdentity(p.Context, &id, update) if err != nil { return translateStoreError(err) } logger.Tracef("DeleteSSHKeys complete") return nil } // UserToken returns a token, in the form of a macaroon, identifying // the user. This token can only be generated by an administrator. func (h *handler) UserToken(p httprequest.Params, r *params.UserTokenRequest) (*bakery.Macaroon, error) { logger.Tracef("UserToken %#v", r) id, err := h.params.Authorizer.Identity(p.Context, &store.Identity{ Username: string(r.Username), }) if err != nil { return nil, errgo.Mask(err, errgo.Is(params.ErrNotFound)) } m, err := h.params.Oven.NewMacaroon( p.Context, httpbakery.RequestVersion(p.Request), []checkers.Caveat{ candidclient.UserDeclaration(id.Id()), checkers.TimeBeforeCaveat(time.Now().Add(h.params.APIMacaroonTimeout)), }, identchecker.LoginOp, ) if err != nil { return nil, errgo.Notef(err, "cannot mint macaroon") } logger.Tracef("UserToken response %#v", m) return m, nil } // VerifyToken verifies that the given token is a macaroon generated by // this service and returns any declared values. func (h *handler) VerifyToken(p httprequest.Params, r *params.VerifyTokenRequest) (map[string]string, error) { logger.Tracef("VerifyToken %#v", r) authInfo, err := h.params.Authorizer.Auth(p.Context, []macaroon.Slice{r.Macaroons}, identchecker.LoginOp) if err != nil { // TODO only return ErrForbidden when the error is because of bad macaroons. return nil, errgo.WithCausef(err, params.ErrForbidden, `verification failure`) } resp := map[string]string{ "username": authInfo.Identity.Id(), } logger.Tracef("VerifyToken response %#v", resp) return resp, nil } // UserExtraInfo returns any stored extra-info for the given user. func (h *handler) UserExtraInfo(p httprequest.Params, r *params.UserExtraInfoRequest) (map[string]interface{}, error) { logger.Tracef("UserExtraInfo %#v", r) id := store.Identity{ Username: string(r.Username), } if err := h.params.Store.Identity(p.Context, &id); err != nil { return nil, translateStoreError(err) } res := make(map[string]interface{}, len(id.ExtraInfo)) for k, v := range id.ExtraInfo { if k == "sshkeys" { continue } jmsg := json.RawMessage(v[0]) res[k] = &jmsg } logger.Tracef("UserExtraInfo response %#v", res) return res, nil } // SetUserExtraInfo updates extra-info for the given user. For each // specified extra-info field the stored values will be updated to be the // specified value. All other values will remain unchanged. func (h *handler) SetUserExtraInfo(p httprequest.Params, r *params.SetUserExtraInfoRequest) error { logger.Tracef("SetUserExtraInfo %#v", r) id := store.Identity{ Username: string(r.Username), ExtraInfo: make(map[string][]string, len(r.ExtraInfo)), } for k, v := range r.ExtraInfo { if err := checkExtraInfoKey(k); err != nil { return errgo.Mask(err, errgo.Is(params.ErrBadRequest)) } buf, err := json.Marshal(v) if err != nil { // This should not be possible as it was only just unmarshalled. panic(err) } id.ExtraInfo[k] = []string{string(buf)} } err := h.params.Store.UpdateIdentity(p.Context, &id, store.Update{store.ExtraInfo: store.Set}) if err != nil { return translateStoreError(err) } logger.Tracef("SetUserExtraInfo complete") return nil } // UserExtraInfoItem returns any stored extra-info item with the given // key for the given user. func (h *handler) UserExtraInfoItem(p httprequest.Params, r *params.UserExtraInfoItemRequest) (interface{}, error) { logger.Tracef("UserExtraInfoItem %#v", r) id := store.Identity{ Username: string(r.Username), } if err := h.params.Store.Identity(p.Context, &id); err != nil { return nil, translateStoreError(err) } if len(id.ExtraInfo[r.Item]) != 1 { return nil, nil } var v interface{} if err := json.Unmarshal([]byte(id.ExtraInfo[r.Item][0]), &v); err != nil { // if it doesn't unmarshal its probably wasn't json in // the first place, so it probably doesn't matter. return nil, nil } logger.Tracef("UserExtraInfoItem response %#v", v) return v, nil } // SetUserExtraInfoItem updates the stored extra-info item with the given // key for the given user. func (h *handler) SetUserExtraInfoItem(p httprequest.Params, r *params.SetUserExtraInfoItemRequest) error { logger.Tracef("SetUserExtraInfoItem %#v", r) id := store.Identity{ Username: string(r.Username), } if err := checkExtraInfoKey(r.Item); err != nil { return errgo.Mask(err, errgo.Is(params.ErrBadRequest)) } buf, err := json.Marshal(r.Data) if err != nil { // This should not be possible as it was only just unmarshalled. panic(err) } id.ExtraInfo = map[string][]string{r.Item: {string(buf)}} err = h.params.Store.UpdateIdentity(p.Context, &id, store.Update{store.ExtraInfo: store.Set}) if err != nil { return translateStoreError(err) } logger.Tracef("SetUserExtraInfoItem complete") return nil } func checkExtraInfoKey(key string) error { if strings.ContainsAny(key, "./$") { return errgo.WithCausef(nil, params.ErrBadRequest, "%q bad key for extra-info", key) } return nil } func (h *handler) userFromIdentity(ctx context.Context, id *auth.Identity) (*params.User, error) { publicKeys := make([]*bakery.PublicKey, len(id.PublicKeys)) for i, key := range id.PublicKeys { pk := key publicKeys[i] = &pk } groups, err := id.Groups(ctx) if err != nil { return nil, errgo.Mask(err) } if groups == nil { // Ensure that a null list of groups is never sent. groups = []string{} } var owner params.Username var externalID string if id.Owner != "" { ownerIdentity := store.Identity{ ProviderID: id.Owner, } err := h.params.Store.Identity(ctx, &ownerIdentity) if err != nil { return nil, errgo.Mask(err) } owner = params.Username(ownerIdentity.Username) } else { externalID = string(id.ProviderID) } var sshKeys []string if len(id.ExtraInfo["sshkeys"]) > 0 { sshKeys = id.ExtraInfo["sshkeys"] } var lastLogin *time.Time if !id.LastLogin.IsZero() { lastLogin = &id.LastLogin } var lastDischarge *time.Time if !id.LastDischarge.IsZero() { lastDischarge = &id.LastDischarge } return ¶ms.User{ Username: params.Username(id.Username), ExternalID: externalID, FullName: id.Name, Email: id.Email, GravatarID: gravatarHash(id.Email), IDPGroups: groups, Owner: owner, PublicKeys: publicKeys, SSHKeys: sshKeys, LastLogin: lastLogin, LastDischarge: lastDischarge, }, nil } func validateUsername(u *params.SetUserRequest) error { if disallowedUsernames[u.Username] { return errgo.Newf("username %q is reserved", u.Username) } if u.User.Owner != "" && !strings.HasSuffix(string(u.Username), "@"+string(u.User.Owner)) { return errgo.Newf(`%s cannot create user %q (suffix must be "@%s")`, u.User.Owner, u.Username, u.User.Owner) } return nil } func publicKeys(pks []*bakery.PublicKey) ([]bakery.PublicKey, error) { pks2 := make([]bakery.PublicKey, len(pks)) for i, pk := range pks { if pk == nil { return nil, errgo.New("null public key provided") } pks2[i] = *pk } return pks2, nil } // gravatarHash calculates the gravatar hash based on the following // specification : https://en.gravatar.com/site/implement/hash func gravatarHash(s string) string { if s == "" { return "" } hasher := md5.New() hasher.Write([]byte(strings.ToLower(strings.TrimSpace(s)))) return fmt.Sprintf("%x", hasher.Sum(nil)) } func translateStoreError(err error) error { var cause error switch errgo.Cause(err) { case store.ErrNotFound: cause = params.ErrNotFound case store.ErrDuplicateUsername: cause = params.ErrAlreadyExists case nil: return nil } err1 := errgo.WithCausef(err, cause, "").(*errgo.Err) err1.SetLocation(1) return err1 } // DischargeTokenForUser allows an administrator to create a discharge // token for the specified user. func (h *handler) DischargeTokenForUser(p httprequest.Params, req *params.DischargeTokenForUserRequest) (params.DischargeTokenForUserResponse, error) { logger.Tracef("DischargeTokenForUser %#v", req) err := h.params.Store.Identity(p.Context, &store.Identity{ Username: string(req.Username), }) if err != nil { return params.DischargeTokenForUserResponse{}, errgo.NoteMask(err, "cannot get identity", errgo.Is(params.ErrNotFound)) } m, err := h.params.Oven.NewMacaroon( p.Context, httpbakery.RequestVersion(p.Request), []checkers.Caveat{ checkers.TimeBeforeCaveat(time.Now().Add(h.params.DischargeTokenTimeout)), candidclient.UserDeclaration(string(req.Username)), }, identchecker.LoginOp, ) if err != nil { return params.DischargeTokenForUserResponse{}, errgo.NoteMask(err, "cannot create discharge token", errgo.Any) } resp := params.DischargeTokenForUserResponse{ DischargeToken: m, } logger.Tracef("DischargeTokenForUser response %#v", resp) return resp, nil } // checkAuthIdentityIsMemberOf checks that the given identity is a member // of all the given groups. func checkAuthIdentityIsMemberOf(ctx context.Context, identity *auth.Identity, groups []string) error { // Note that the admin user is considered a member of all groups. if identity.Id() == auth.AdminUsername { // Admin is a member of all groups by definition. return nil } identityGroups, err := identity.Groups(ctx) if err != nil { return errgo.Notef(err, "cannot get groups for authenticated user") } for _, g := range groups { found := false for _, idg := range identityGroups { if idg == g { found = true break } } if !found { return errgo.WithCausef(nil, params.ErrForbidden, "cannot add agent to groups that you are not a member of") } } return nil } func newAgentName() (string, error) { buf := make([]byte, 16) _, err := rand.Read(buf) if err != nil { return "", errgo.Mask(err) } return fmt.Sprintf("a-%x", buf), nil } // GetUserWithID returns the user information for the request user. func (h *handler) GetUserWithID(p httprequest.Params, req *params.GetUserWithIDRequest) (*params.User, error) { logger.Tracef("GetUserWithID %#v", req) id, err := h.params.Authorizer.Identity(p.Context, &store.Identity{ ProviderID: store.ProviderIdentity(req.UserID), }) if err != nil { return nil, errgo.Mask(err, errgo.Is(params.ErrNotFound)) } u, err := h.userFromIdentity(p.Context, id) if err != nil { return nil, errgo.Mask(err) } logger.Tracef("User response %#v", u) return u, nil } // GetUserGroupsWithID returns the groups for a user with the given ID. func (h *handler) GetUserGroupsWithID(p httprequest.Params, req *params.GetUserGroupsWithIDRequest) (*params.GroupsResponse, error) { logger.Tracef("GetUserGroupsWithID %#v", req) id, err := h.params.Authorizer.Identity(p.Context, &store.Identity{ ProviderID: store.ProviderIdentity(req.UserID), }) if err != nil { return nil, errgo.Mask(err, errgo.Is(params.ErrNotFound)) } groups, err := id.Groups(p.Context) if err != nil { return nil, errgo.Mask(err) } if groups == nil { groups = []string{} } logger.Tracef("UserGroups response %#v", groups) return ¶ms.GroupsResponse{ Groups: groups, }, nil } golang-github-canonical-candid-1.12.3/internal/v1/users_test.go000066400000000000000000001067471457263123000244050ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package v1_test import ( "context" "fmt" "strings" "testing" "time" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "gopkg.in/httprequest.v1" macaroon "gopkg.in/macaroon.v2" "github.com/canonical/candid/candidclient" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/static" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/internal/discharger" "github.com/canonical/candid/internal/identity" v1 "github.com/canonical/candid/internal/v1" "github.com/canonical/candid/params" "github.com/canonical/candid/store" ) func TestUsersAPI(t *testing.T) { qtsuite.Run(qt.New(t), &usersSuite{}) } type usersSuite struct { store *candidtest.Store srv *candidtest.Server adminClient *candidclient.Client interactor httpbakery.WebBrowserInteractor } func (s *usersSuite) Init(c *qt.C) { s.store = candidtest.NewStore() sp := s.store.ServerParams() // Ensure that there's an identity provider for the test identities // we add so that group resolution on test identities works correctly. sp.IdentityProviders = []idp.IdentityProvider{ static.NewIdentityProvider(static.Params{ Name: "test", Users: map[string]static.UserInfo{ "bob": { Password: "bobpassword", Groups: []string{"g1", "g2", "testgroup"}, }, }, }), } s.srv = candidtest.NewServer(c, sp, map[string]identity.NewAPIHandlerFunc{ "discharger": discharger.NewAPIHandler, "v1": v1.NewAPIHandler, }) s.adminClient = s.srv.AdminIdentityClient(false) s.interactor = httpbakery.WebBrowserInteractor{ OpenWebBrowser: candidtest.PasswordLogin(c, "bob", "bobpassword"), } } func (s *usersSuite) TestRemoveUserMFACredentials(c *qt.C) { user := params.User{ Username: "jbloggs", ExternalID: "test:http://example.com/jbloggs", FullName: "Joe Bloggs", Email: "jbloggs@example.com", IDPGroups: []string{ "test", }, } s.addUser(c, user) cred := store.MFACredential{ ID: []byte("test-id"), Name: "test credential", ProviderID: store.ProviderIdentity(user.ExternalID), AttestationType: "test", AuthenticatorGUID: []byte("test guid"), AuthenticatorSignCount: 1, } err := s.store.Store.AddMFACredential(context.Background(), cred) c.Assert(err, qt.Equals, nil) creds, err := s.store.Store.UserMFACredentials(context.Background(), user.ExternalID) c.Assert(err, qt.Equals, nil) c.Assert(creds, qt.DeepEquals, []store.MFACredential{cred}) err = s.adminClient.ClearUserMFACredentials(context.Background(), ¶ms.ClearUserMFACredentialsRequest{ Username: user.Username, }, ) c.Assert(err, qt.Equals, nil) creds, err = s.store.Store.UserMFACredentials(context.Background(), user.ExternalID) c.Assert(err, qt.Equals, nil) c.Assert(creds, qt.HasLen, 0) err = s.adminClient.ClearUserMFACredentials(context.Background(), ¶ms.ClearUserMFACredentialsRequest{ Username: user.Username, }, ) c.Assert(err, qt.Equals, nil) client := s.srv.IdentityClient(c, "a-bob@candid", "bob") err = client.ClearUserMFACredentials(context.Background(), ¶ms.ClearUserMFACredentialsRequest{ Username: user.Username, }, ) c.Assert(err, qt.ErrorMatches, ".* permission denied") } func (s *usersSuite) TestRoundTripUser(c *qt.C) { user := params.User{ Username: "jbloggs", ExternalID: "test:http://example.com/jbloggs", FullName: "Joe Bloggs", Email: "jbloggs@example.com", IDPGroups: []string{ "test", }, } s.addUser(c, user) resp, err := s.adminClient.User(s.srv.Ctx, ¶ms.UserRequest{ Username: user.Username, }) c.Assert(err, qt.IsNil) s.assertUser(c, *resp, user) } func (s *usersSuite) TestUsernameContainingUnderscore(c *qt.C) { user := params.User{ Username: "jbloggs_TEST", ExternalID: "test:http://example.com/jbloggs", FullName: "Joe Bloggs", Email: "jbloggs@example.com", IDPGroups: []string{ "test", }, } s.addUser(c, user) resp, err := s.adminClient.User(s.srv.Ctx, ¶ms.UserRequest{ Username: user.Username, }) c.Assert(err, qt.IsNil) s.assertUser(c, *resp, user) } var userErrorTests = []struct { about string username params.Username expectError string }{{ about: "not found", username: "not-there", expectError: `Get .*/v1/u/not-there: user not-there not found`, }, { about: "bad username", username: "verylongname_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", expectError: `Get .*/v1/u/verylongname_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: cannot unmarshal parameters: cannot unmarshal into field Username: username longer than 256 characters`, }} func (s *usersSuite) TestUserErrors(c *qt.C) { for _, test := range userErrorTests { c.Run(test.about, func(c *qt.C) { _, err := s.adminClient.User(s.srv.Ctx, ¶ms.UserRequest{ Username: test.username, }) c.Assert(err, qt.ErrorMatches, test.expectError) }) } } var ( privKey1 = bakery.MustGenerateKey() pk1 = privKey1.Public privKey2 = bakery.MustGenerateKey() pk2 = privKey2.Public ) func (s *usersSuite) TestCreateAgent(c *qt.C) { client, err := candidclient.New(candidclient.NewParams{ BaseURL: s.srv.URL, Client: s.srv.Client(s.interactor), }) c.Assert(err, qt.IsNil) resp, err := client.CreateAgent(s.srv.Ctx, ¶ms.CreateAgentRequest{ CreateAgentBody: params.CreateAgentBody{ FullName: "my agent", PublicKeys: []*bakery.PublicKey{&pk1}, }, }) c.Assert(err, qt.IsNil) if !strings.HasPrefix(string(resp.Username), "a-") { c.Errorf("unexpected agent username %q", resp.Username) } agentClient, err := candidclient.New(candidclient.NewParams{ BaseURL: s.srv.URL, Client: &httpbakery.Client{ Client: httpbakery.NewHTTPClient(), Key: privKey1, }, AgentUsername: string(resp.Username), }) c.Assert(err, qt.IsNil) whoAmIResp, err := agentClient.WhoAmI(s.srv.Ctx, nil) c.Assert(err, qt.IsNil) c.Assert(whoAmIResp.User, qt.Equals, string(resp.Username)) groups, err := agentClient.UserGroups(s.srv.Ctx, ¶ms.UserGroupsRequest{ Username: resp.Username, }) c.Assert(err, qt.IsNil) c.Assert(groups, qt.HasLen, 0) } func (s *usersSuite) TestCreateAgentAsAgent(c *qt.C) { client := s.srv.IdentityClient(c, "testagent@candid", "testgroup") _, err := client.CreateAgent(s.srv.Ctx, ¶ms.CreateAgentRequest{ CreateAgentBody: params.CreateAgentBody{ FullName: "my agent", PublicKeys: []*bakery.PublicKey{&pk1}, }, }) c.Assert(err, qt.ErrorMatches, `Post.*: cannot create an agent using an agent account`) } func (s *usersSuite) TestCreateAgentWithGroups(c *qt.C) { client, err := candidclient.New(candidclient.NewParams{ BaseURL: s.srv.URL, Client: s.srv.Client(s.interactor), }) c.Assert(err, qt.IsNil) // We can't create agents in groups that aren't in the owner's // group list. resp, err := client.CreateAgent(s.srv.Ctx, ¶ms.CreateAgentRequest{ CreateAgentBody: params.CreateAgentBody{ PublicKeys: []*bakery.PublicKey{&pk1}, Groups: []string{"g1", "other", "g2"}, }, }) c.Assert(err, qt.ErrorMatches, `Post .*: cannot add agent to groups that you are not a member of`) s.setUserGroups(c, "bob", "g3") // We can create agents in groups that are a subset of the // owner's groups. resp, err = client.CreateAgent(s.srv.Ctx, ¶ms.CreateAgentRequest{ CreateAgentBody: params.CreateAgentBody{ PublicKeys: []*bakery.PublicKey{&pk1}, Groups: []string{"g1", "g3"}, }, }) c.Assert(err, qt.IsNil) // If the owner is removed from a group, the agent won't be // in that group any more. s.setUserGroups(c, "bob") groups, err := s.adminClient.UserGroups(s.srv.Ctx, ¶ms.UserGroupsRequest{ Username: resp.Username, }) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"g1"}) // If the owner is added back to the group, the agent // gets added back too. s.setUserGroups(c, "bob", "g3", "g4") groups, err = s.adminClient.UserGroups(s.srv.Ctx, ¶ms.UserGroupsRequest{ Username: resp.Username, }) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"g1", "g3"}) } func (s *usersSuite) setUserGroups(c *qt.C, username string, groups ...string) { err := s.store.Store.UpdateIdentity(s.srv.Ctx, &store.Identity{ Username: username, Groups: groups, }, store.Update{ store.Groups: store.Set, }) c.Assert(err, qt.IsNil) } func (s *usersSuite) TestCreateParentAgent(c *qt.C) { resp, err := s.adminClient.CreateAgent(s.srv.Ctx, ¶ms.CreateAgentRequest{ CreateAgentBody: params.CreateAgentBody{ FullName: "my agent", PublicKeys: []*bakery.PublicKey{&pk1}, Groups: []string{"g1", "g2"}, Parent: true, }, }) c.Assert(err, qt.IsNil) if !strings.HasPrefix(string(resp.Username), "a-") { c.Errorf("unexpected agent username %q", resp.Username) } systemUserClient, err := candidclient.New(candidclient.NewParams{ BaseURL: s.srv.URL, Client: &httpbakery.Client{ Client: httpbakery.NewHTTPClient(), Key: privKey1, }, AgentUsername: string(resp.Username), }) c.Assert(err, qt.IsNil) whoAmIResp, err := systemUserClient.WhoAmI(s.srv.Ctx, nil) c.Assert(err, qt.IsNil) c.Assert(whoAmIResp.User, qt.Equals, string(resp.Username)) groups, err := systemUserClient.UserGroups(s.srv.Ctx, ¶ms.UserGroupsRequest{ Username: resp.Username, }) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"g1", "g2"}) } func (s *usersSuite) TestCreateParentAgentUnauthorized(c *qt.C) { client, err := candidclient.New(candidclient.NewParams{ BaseURL: s.srv.URL, Client: s.srv.Client(s.interactor), }) c.Assert(err, qt.IsNil) _, err = client.CreateAgent(s.srv.Ctx, ¶ms.CreateAgentRequest{ CreateAgentBody: params.CreateAgentBody{ FullName: "my agent", PublicKeys: []*bakery.PublicKey{&pk1}, Groups: []string{"g1", "g2"}, Parent: true, }, }) c.Assert(err, qt.ErrorMatches, `Post http://.*/v1/u: permission denied`) } func (s *usersSuite) TestCreateParentAgentNotInGroups(c *qt.C) { client, err := candidclient.New(candidclient.NewParams{ BaseURL: s.srv.URL, Client: s.srv.Client(s.interactor), }) c.Assert(err, qt.IsNil) err = s.store.ACLStore.Add(s.srv.Ctx, "write-user", []string{"bob"}) c.Assert(err, qt.IsNil) _, err = client.CreateAgent(s.srv.Ctx, ¶ms.CreateAgentRequest{ CreateAgentBody: params.CreateAgentBody{ FullName: "my agent", PublicKeys: []*bakery.PublicKey{&pk1}, Groups: []string{"g1", "g3"}, Parent: true, }, }) c.Assert(err, qt.ErrorMatches, `Post http://.*/v1/u: cannot add agent to groups that you are not a member of`) } func (s *usersSuite) TestCreateAgentAsParentAgent(c *qt.C) { resp, err := s.adminClient.CreateAgent(s.srv.Ctx, ¶ms.CreateAgentRequest{ CreateAgentBody: params.CreateAgentBody{ FullName: "my agent", PublicKeys: []*bakery.PublicKey{&pk1}, Parent: true, }, }) c.Assert(err, qt.IsNil) if !strings.HasPrefix(string(resp.Username), "a-") { c.Errorf("unexpected agent username %q", resp.Username) } systemUserClient, err := candidclient.New(candidclient.NewParams{ BaseURL: s.srv.URL, Client: &httpbakery.Client{ Client: httpbakery.NewHTTPClient(), Key: privKey1, }, AgentUsername: string(resp.Username), }) c.Assert(err, qt.IsNil) resp, err = systemUserClient.CreateAgent(s.srv.Ctx, ¶ms.CreateAgentRequest{ CreateAgentBody: params.CreateAgentBody{ FullName: "my agent 2", PublicKeys: []*bakery.PublicKey{&pk1}, }, }) c.Assert(err, qt.IsNil) client, err := candidclient.New(candidclient.NewParams{ BaseURL: s.srv.URL, Client: &httpbakery.Client{ Client: httpbakery.NewHTTPClient(), Key: privKey1, }, AgentUsername: string(resp.Username), }) c.Assert(err, qt.IsNil) _, err = client.CreateAgent(s.srv.Ctx, ¶ms.CreateAgentRequest{ CreateAgentBody: params.CreateAgentBody{ FullName: "my agent", PublicKeys: []*bakery.PublicKey{&pk1}, }, }) c.Assert(err, qt.ErrorMatches, `Post.*: cannot create an agent using an agent account`) } func (s *usersSuite) clearIdentities(c *qt.C) { store, ok := s.store.Store.(interface { RemoveAll() }) if !ok { c.Fatalf("store type %T does not implement RemoveAll", s.store.Store) } store.RemoveAll() } var queryUserTests = []struct { about string externalID string email string lastLoginSince time.Time lastDIschargeSince time.Time expect []string }{{ about: "query existing user", externalID: "test:http://example.com/jbloggs2", expect: []string{"jbloggs2"}, }, { about: "query non-existing user", externalID: "test:http://example.com/jbloggs", expect: []string{}, }, { about: "no query parameter", expect: []string{auth.AdminUsername, "jbloggs2"}, }, { about: "query email", email: "jbloggs2@example.com", expect: []string{"jbloggs2"}, }, { about: "query email not found", email: "not-there@example.com", expect: []string{}, }, { about: "last login in range", externalID: "test:http://example.com/jbloggs2", lastLoginSince: time.Now().AddDate(0, 0, -30), expect: []string{"jbloggs2"}, }, { about: "last login too soon", externalID: "test:http://example.com/jbloggs2", lastLoginSince: time.Now().AddDate(0, 0, -28), expect: []string{}, }, { about: "last discharge in range", externalID: "test:http://example.com/jbloggs2", lastDIschargeSince: time.Now().AddDate(0, 0, -15), expect: []string{"jbloggs2"}, }, { about: "last discharge too soon", externalID: "test:http://example.com/jbloggs2", lastDIschargeSince: time.Now().AddDate(0, 0, -13), expect: []string{}, }, { about: "combined login and discharge (found)", externalID: "test:http://example.com/jbloggs2", lastLoginSince: time.Now().AddDate(0, 0, -30), lastDIschargeSince: time.Now().AddDate(0, 0, -15), expect: []string{"jbloggs2"}, }, { about: "combined login and discharge (not found)", externalID: "test:http://example.com/jbloggs2", lastLoginSince: time.Now().AddDate(0, 0, -30), lastDIschargeSince: time.Now().AddDate(0, 0, -13), expect: []string{}, }} func (s *usersSuite) TestQueryUsers(c *qt.C) { err := s.store.Store.UpdateIdentity( s.srv.Ctx, &store.Identity{ Username: "jbloggs2", ProviderID: "test:http://example.com/jbloggs2", Name: "Joe Bloggs II", Email: "jbloggs2@example.com", LastLogin: time.Now().AddDate(0, 0, -29), LastDischarge: time.Now().AddDate(0, 0, -14), Groups: []string{ "test", }, }, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Groups: store.Set, store.Email: store.Set, store.LastLogin: store.Set, store.LastDischarge: store.Set, }, ) c.Assert(err, qt.IsNil) for _, test := range queryUserTests { c.Run(test.about, func(c *qt.C) { req := params.QueryUsersRequest{ ExternalID: test.externalID, Email: test.email, } if !test.lastLoginSince.IsZero() { req.LastLoginSince = test.lastLoginSince.Format(time.RFC3339Nano) } if !test.lastDIschargeSince.IsZero() { req.LastDischargeSince = test.lastDIschargeSince.Format(time.RFC3339Nano) } users, err := s.adminClient.QueryUsers(s.srv.Ctx, &req) c.Assert(err, qt.IsNil) c.Assert(users, qt.DeepEquals, test.expect) }) } } func (s *usersSuite) TestQueryUsersBadLastLogin(c *qt.C) { _, err := s.adminClient.QueryUsers(s.srv.Ctx, ¶ms.QueryUsersRequest{ LastLoginSince: "yesterday", }) c.Assert(err, qt.ErrorMatches, `Get http://.*/v1/u?.*last-login-since=yesterday.*: cannot unmarshal last-login-since: parsing time "yesterday" as "2006-01-02T15:04:05Z07:00": cannot parse "yesterday" as "2006"`) } func (s *usersSuite) TestQueryUsersBadLastDischarge(c *qt.C) { _, err := s.adminClient.QueryUsers(s.srv.Ctx, ¶ms.QueryUsersRequest{ LastDischargeSince: "yesterday", }) c.Assert(err, qt.ErrorMatches, `Get http://.*/v1/u?.*last-discharge-since=yesterday.*: cannot unmarshal last-discharge-since: parsing time "yesterday" as "2006-01-02T15:04:05Z07:00": cannot parse "yesterday" as "2006"`) } func (s *usersSuite) TestQueryUsersUnauthorized(c *qt.C) { client := s.srv.IdentityClient(c, "a-bob@candid", "bob") _, err := client.QueryUsers(s.srv.Ctx, ¶ms.QueryUsersRequest{}) c.Assert(err, qt.ErrorMatches, `Get http://.*/v1/u?.*: permission denied`) } func (s *usersSuite) TestQueryAgentUsers(c *qt.C) { err := s.store.Store.UpdateIdentity( s.srv.Ctx, &store.Identity{ Username: "jbloggs2", ProviderID: "test:http://example.com/jbloggs2", Name: "Joe Bloggs II", Email: "jbloggs2@example.com", LastLogin: time.Now().AddDate(0, 0, -29), LastDischarge: time.Now().AddDate(0, 0, -14), Groups: []string{ "test", }, }, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Groups: store.Set, store.Email: store.Set, store.LastLogin: store.Set, store.LastDischarge: store.Set, }, ) c.Assert(err, qt.IsNil) err = s.store.Store.UpdateIdentity( s.srv.Ctx, &store.Identity{ Username: "a-agent@candid", ProviderID: "idm:a-agent", Owner: "test:http://example.com/jbloggs2", }, store.Update{ store.Username: store.Set, store.Owner: store.Set, }, ) c.Assert(err, qt.IsNil) client := s.srv.IdentityClient(c, "a-jbloggs2@candid", "jbloggs2") users, err := client.QueryUsers(s.srv.Ctx, ¶ms.QueryUsersRequest{ Owner: "jbloggs2", }) c.Assert(err, qt.IsNil) c.Assert(users, qt.DeepEquals, []string{"a-agent@candid"}) } func (s *usersSuite) TestQueryAgentUsersOwnerNotFound(c *qt.C) { client := s.srv.IdentityClient(c, "a-jbloggs2@candid", "test") users, err := client.QueryUsers(s.srv.Ctx, ¶ms.QueryUsersRequest{ Owner: "test", }) c.Assert(err, qt.IsNil) c.Assert(users, qt.DeepEquals, []string{}) } func (s *usersSuite) TestSSHKeys(c *qt.C) { s.addUser(c, params.User{ Username: "jbloggs", ExternalID: "http://example.com/jbloggs", Email: "jbloggs@example.com", FullName: "Joe Bloggs", IDPGroups: []string{ "test", }, }) // Check there is no ssh key for the user. sshKeys, err := s.adminClient.GetSSHKeys(s.srv.Ctx, ¶ms.SSHKeysRequest{ Username: "jbloggs", }) c.Assert(err, qt.IsNil) c.Assert(sshKeys.SSHKeys, qt.DeepEquals, []string(nil)) // Add ssh keys to the user. err = s.adminClient.PutSSHKeys(s.srv.Ctx, ¶ms.PutSSHKeysRequest{ Username: "jbloggs", Body: params.PutSSHKeysBody{ SSHKeys: []string{"36ASDER56", "22ERT56DG", "56ASDFASDF32"}, Add: false, }, }) c.Assert(err, qt.IsNil) // Check it is present. sshKeys, err = s.adminClient.GetSSHKeys(s.srv.Ctx, ¶ms.SSHKeysRequest{ Username: "jbloggs", }) c.Assert(err, qt.IsNil) c.Assert(sshKeys.SSHKeys, qt.DeepEquals, []string{ "36ASDER56", "22ERT56DG", "56ASDFASDF32", }) // Remove some ssh keys. err = s.adminClient.DeleteSSHKeys(s.srv.Ctx, ¶ms.DeleteSSHKeysRequest{ Username: "jbloggs", Body: params.DeleteSSHKeysBody{ SSHKeys: []string{"22ERT56DG", "56ASDFASDF32"}, }, }) c.Assert(err, qt.IsNil) // Check we only get one. sshKeys, err = s.adminClient.GetSSHKeys(s.srv.Ctx, ¶ms.SSHKeysRequest{ Username: "jbloggs", }) c.Assert(err, qt.IsNil) c.Assert(sshKeys.SSHKeys, qt.DeepEquals, []string{ "36ASDER56", }) // Delete an unknown ssh key just do nothing silently. err = s.adminClient.DeleteSSHKeys(s.srv.Ctx, ¶ms.DeleteSSHKeysRequest{ Username: "jbloggs", Body: params.DeleteSSHKeysBody{ SSHKeys: []string{"22ERT56DG"}, }, }) c.Assert(err, qt.IsNil) // Check we only get one. sshKeys, err = s.adminClient.GetSSHKeys(s.srv.Ctx, ¶ms.SSHKeysRequest{ Username: "jbloggs", }) c.Assert(err, qt.IsNil) c.Assert(sshKeys.SSHKeys, qt.DeepEquals, []string{ "36ASDER56", }) // Append one ssh key. err = s.adminClient.PutSSHKeys(s.srv.Ctx, ¶ms.PutSSHKeysRequest{ Username: "jbloggs", Body: params.PutSSHKeysBody{ SSHKeys: []string{"90SDFGS45"}, Add: true, }, }) c.Assert(err, qt.IsNil) // Check we get two. sshKeys, err = s.adminClient.GetSSHKeys(s.srv.Ctx, ¶ms.SSHKeysRequest{ Username: "jbloggs", }) c.Assert(err, qt.IsNil) c.Assert(sshKeys.SSHKeys, qt.DeepEquals, []string{ "36ASDER56", "90SDFGS45", }) } func (s *usersSuite) TestVerifyUserToken(c *qt.C) { s.addUser(c, params.User{ Username: "jbloggs", ExternalID: "http://example.com/jbloggs", Email: "jbloggs@example.com", FullName: "Joe Bloggs", IDPGroups: []string{ "test", }, }) m, err := s.adminClient.UserToken(s.srv.Ctx, ¶ms.UserTokenRequest{ Username: "jbloggs", }) c.Assert(err, qt.IsNil) declared, err := s.adminClient.VerifyToken(s.srv.Ctx, ¶ms.VerifyTokenRequest{ Macaroons: macaroon.Slice{m.M()}, }) c.Assert(err, qt.IsNil) c.Assert(declared, qt.DeepEquals, map[string]string{ "username": "jbloggs", }) badm, err := macaroon.New([]byte{}, []byte("no such macaroon"), "loc", macaroon.LatestVersion) c.Assert(err, qt.IsNil) _, err = s.adminClient.VerifyToken(s.srv.Ctx, ¶ms.VerifyTokenRequest{ Macaroons: macaroon.Slice{badm}, }) c.Assert(err, qt.ErrorMatches, `Post .*/v1/verify: verification failure: macaroon discharge required: authentication required`) } func (s *usersSuite) TestUserTokenNotFound(c *qt.C) { _, err := s.adminClient.UserToken(s.srv.Ctx, ¶ms.UserTokenRequest{ Username: "not-there", }) c.Assert(err, qt.ErrorMatches, `Get .*/v1/u/not-there/macaroon: user not-there not found`) } func (s *usersSuite) TestDischargeToken(c *qt.C) { s.addUser(c, params.User{ Username: "jbloggs", ExternalID: "http://example.com/jbloggs", Email: "jbloggs@example.com", FullName: "Joe Bloggs", IDPGroups: []string{ "test", }, }) client := &httprequest.Client{ BaseURL: s.srv.URL, Doer: s.srv.AdminClient(), } var resp params.DischargeTokenForUserResponse err := client.Get(s.srv.Ctx, "/v1/discharge-token-for-user?username=jbloggs", &resp) c.Assert(err, qt.IsNil) declared, err := s.adminClient.VerifyToken(s.srv.Ctx, ¶ms.VerifyTokenRequest{ Macaroons: macaroon.Slice{resp.DischargeToken.M()}, }) c.Assert(err, qt.IsNil) c.Assert(declared, qt.DeepEquals, map[string]string{ "username": "jbloggs", }) } var userGroupTests = []struct { about string username params.Username expectGroups []string expectError string }{{ about: "no groups", username: "jbloggs", expectGroups: []string{}, }, { about: "groups", username: "jbloggs2", expectGroups: []string{"test1", "test2"}, }, { about: "no such user", username: "not-there", expectError: `Get .*/v1/u/not-there/groups: user not-there not found`, }} func (s *usersSuite) TestUserGroups(c *qt.C) { s.addUser(c, params.User{ Username: "jbloggs", ExternalID: "http://example.com/jbloggs", Email: "jbloggs@example.com", FullName: "Joe Bloggs", }) s.addUser(c, params.User{ Username: "jbloggs2", ExternalID: "http://example.com/jbloggs2", Email: "jbloggs2@example.com", FullName: "Joe Bloggs II", IDPGroups: []string{ "test1", "test2", }, }) for _, test := range userGroupTests { c.Run(test.about, func(c *qt.C) { groups, err := s.adminClient.UserGroups(s.srv.Ctx, ¶ms.UserGroupsRequest{ Username: test.username, }) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) return } c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, test.expectGroups) }) } } func (s *usersSuite) TestSetUserGroups(c *qt.C) { s.addUser(c, params.User{ Username: "jbloggs", ExternalID: "http://example.com/jbloggs", Email: "jbloggs@example.com", FullName: "Joe Bloggs", IDPGroups: []string{ "test1", "test2", }, }) err := s.adminClient.SetUserGroups(s.srv.Ctx, ¶ms.SetUserGroupsRequest{ Username: "jbloggs", Groups: params.Groups{Groups: []string{"test3", "test4"}}, }) c.Assert(err, qt.IsNil) groups, err := s.adminClient.UserGroups(s.srv.Ctx, ¶ms.UserGroupsRequest{ Username: "jbloggs", }) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"test3", "test4"}) err = s.adminClient.SetUserGroups(s.srv.Ctx, ¶ms.SetUserGroupsRequest{ Username: "not-there", Groups: params.Groups{Groups: []string{"test3", "test4"}}, }) c.Assert(err, qt.ErrorMatches, `Put .*/v1/u/not-there/groups: user not-there not found`) } var modifyUserGroupsTests = []struct { about string startGroups []string username params.Username addGroups []string removeGroups []string expectGroups []string expectError string }{{ about: "add groups", startGroups: []string{"test1", "test2"}, addGroups: []string{"test3", "test4"}, expectGroups: []string{"test1", "test2", "test3", "test4"}, }, { about: "remove groups", startGroups: []string{"test1", "test2"}, removeGroups: []string{"test1", "test2"}, expectGroups: []string{}, }, { about: "add and remove groups", startGroups: []string{"test1", "test2"}, addGroups: []string{"test3", "test4"}, removeGroups: []string{"test1", "test2"}, expectError: `Post .*/v1/u/.*/groups: cannot add and remove groups in the same operation`, }, { about: "remove groups not a member of", startGroups: []string{"test1", "test2"}, removeGroups: []string{"test5"}, expectGroups: []string{"test1", "test2"}, }, { about: "user not found", username: "not-there", addGroups: []string{"test3", "test4"}, expectError: `Post .*/v1/u/not-there/groups: user not-there not found`, }} func (s *usersSuite) TestModifyUserGroups(c *qt.C) { for i, test := range modifyUserGroupsTests { c.Run(test.about, func(c *qt.C) { username := params.Username(fmt.Sprintf("test-%d", i)) if test.username == "" { test.username = username } s.addUser(c, params.User{ Username: username, ExternalID: "test:http://example.com/" + string(username), IDPGroups: test.startGroups, }) err := s.adminClient.ModifyUserGroups(s.srv.Ctx, ¶ms.ModifyUserGroupsRequest{ Username: test.username, Groups: params.ModifyGroups{ Add: test.addGroups, Remove: test.removeGroups, }, }) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) return } c.Assert(err, qt.IsNil) groups, err := s.adminClient.UserGroups(s.srv.Ctx, ¶ms.UserGroupsRequest{ Username: test.username, }) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, test.expectGroups) }) } } func (s *usersSuite) TestUserIDPGroups(c *qt.C) { s.addUser(c, params.User{ Username: "jbloggs", ExternalID: "http://example.com/jbloggs", Email: "jbloggs@example.com", FullName: "Joe Bloggs", IDPGroups: []string{ "test1", "test2", }, }) groups, err := s.adminClient.UserIDPGroups(s.srv.Ctx, ¶ms.UserIDPGroupsRequest{ UserGroupsRequest: params.UserGroupsRequest{ Username: "jbloggs", }, }) c.Assert(err, qt.IsNil) c.Assert(groups, qt.DeepEquals, []string{"test1", "test2"}) } func (s *usersSuite) TestWhoAmIWithAuthenticatedUser(c *qt.C) { client := s.srv.IdentityClient(c, "bob@candid") resp, err := client.WhoAmI(s.srv.Ctx, nil) c.Assert(err, qt.IsNil) c.Assert(resp.User, qt.Equals, "bob@candid") } func (s *usersSuite) TestWhoAmIWithNoUser(c *qt.C) { client, err := candidclient.New(candidclient.NewParams{ BaseURL: s.srv.URL, Client: s.srv.Client(nil), }) c.Assert(err, qt.IsNil) _, err = client.WhoAmI(s.srv.Ctx, nil) c.Assert(err, qt.ErrorMatches, `Get .*/v1/whoami: cannot get discharge from ".*": cannot start interactive session: interaction required but not possible`) } func (s *usersSuite) TestExtraInfo(c *qt.C) { s.addUser(c, params.User{ Username: "jbloggs", ExternalID: "http://example.com/jbloggs", }) err := s.adminClient.SetUserExtraInfo(s.srv.Ctx, ¶ms.SetUserExtraInfoRequest{ Username: "jbloggs", ExtraInfo: map[string]interface{}{ "item1": 1, "item2": "two", }, }) c.Assert(err, qt.IsNil) ei, err := s.adminClient.UserExtraInfo(s.srv.Ctx, ¶ms.UserExtraInfoRequest{ Username: "jbloggs", }) c.Assert(err, qt.IsNil) c.Assert(ei, qt.DeepEquals, map[string]interface{}{ "item1": 1.0, "item2": "two", }) err = s.adminClient.SetUserExtraInfo(s.srv.Ctx, ¶ms.SetUserExtraInfoRequest{ Username: "jbloggs", ExtraInfo: map[string]interface{}{ "item1": 2, "item3": "three", }, }) c.Assert(err, qt.IsNil) ei, err = s.adminClient.UserExtraInfo(s.srv.Ctx, ¶ms.UserExtraInfoRequest{ Username: "jbloggs", }) c.Assert(err, qt.IsNil) c.Assert(ei, qt.DeepEquals, map[string]interface{}{ "item1": 2.0, "item2": "two", "item3": "three", }) item, err := s.adminClient.UserExtraInfoItem(s.srv.Ctx, ¶ms.UserExtraInfoItemRequest{ Username: "jbloggs", Item: "item2", }) c.Assert(err, qt.IsNil) c.Assert(item, qt.Equals, "two") err = s.adminClient.SetUserExtraInfoItem(s.srv.Ctx, ¶ms.SetUserExtraInfoItemRequest{ Username: "jbloggs", Item: "item2", Data: "TWO", }) c.Assert(err, qt.IsNil) ei, err = s.adminClient.UserExtraInfo(s.srv.Ctx, ¶ms.UserExtraInfoRequest{ Username: "jbloggs", }) c.Assert(err, qt.IsNil) c.Assert(ei, qt.DeepEquals, map[string]interface{}{ "item1": 2.0, "item2": "TWO", "item3": "three", }) } func (s *usersSuite) TestExtraInfoNotFound(c *qt.C) { err := s.adminClient.SetUserExtraInfo(s.srv.Ctx, ¶ms.SetUserExtraInfoRequest{ Username: "not-there", ExtraInfo: map[string]interface{}{ "item1": 1, "item2": "two", }, }) c.Assert(err, qt.ErrorMatches, `Put .*/v1/u/not-there/extra-info: user not-there not found`) _, err = s.adminClient.UserExtraInfo(s.srv.Ctx, ¶ms.UserExtraInfoRequest{ Username: "not-there", }) c.Assert(err, qt.ErrorMatches, `Get .*/v1/u/not-there/extra-info: user not-there not found`) _, err = s.adminClient.UserExtraInfoItem(s.srv.Ctx, ¶ms.UserExtraInfoItemRequest{ Username: "not-there", Item: "item2", }) c.Assert(err, qt.ErrorMatches, `Get .*/v1/u/not-there/extra-info/item2: user not-there not found`) err = s.adminClient.SetUserExtraInfoItem(s.srv.Ctx, ¶ms.SetUserExtraInfoItemRequest{ Username: "not-there", Item: "item2", Data: "TWO", }) c.Assert(err, qt.ErrorMatches, `Put .*/v1/u/not-there/extra-info/item2: user not-there not found`) } func (s *usersSuite) assertUser(c *qt.C, u1, u2 params.User) { u1.GravatarID = "" u1.LastLogin = nil u1.LastDischarge = nil u2.GravatarID = "" u2.LastLogin = nil u2.LastDischarge = nil c.Assert(len(u1.PublicKeys), qt.Equals, len(u2.PublicKeys), qt.Commentf("mismatch in public keys")) for i, pk := range u1.PublicKeys { c.Assert(pk.Key, qt.Equals, u2.PublicKeys[i].Key) } u1.PublicKeys = nil u2.PublicKeys = nil c.Assert(u1, qt.DeepEquals, u2) } func (s *usersSuite) addUser(c *qt.C, u params.User) { identity := store.Identity{ Username: string(u.Username), ProviderID: store.ProviderIdentity(u.ExternalID), Name: u.FullName, Email: u.Email, Groups: u.IDPGroups, PublicKeys: publicKeys(u.PublicKeys), } if u.Owner != "" { // Note: this mirrors the logic in handler.SetUser. owner := store.Identity{ Username: string(u.Owner), } err := s.store.Store.Identity(s.srv.Ctx, &owner) c.Assert(err, qt.IsNil) identity.Owner = owner.ProviderID } err := s.store.Store.UpdateIdentity(s.srv.Ctx, &identity, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, store.Groups: store.Set, store.PublicKeys: store.Set, store.ProviderInfo: store.Set, store.Owner: store.Set, }) c.Assert(err, qt.IsNil) } func publicKeys(pks []*bakery.PublicKey) []bakery.PublicKey { pks1 := make([]bakery.PublicKey, len(pks)) for i, pk := range pks { if pk == nil { panic("nil public key") } pks1[i] = *pk } return pks1 } func publicKeyPtrs(pks []bakery.PublicKey) []*bakery.PublicKey { pks1 := make([]*bakery.PublicKey, len(pks)) for i, key := range pks { pk := key pks1[i] = &pk } return pks1 } var getUserWithIDTests = []struct { about string userid string expectUser *params.User expectError string }{{ about: "no groups", userid: "test:jbloggs", expectUser: ¶ms.User{ Username: "jbloggs", ExternalID: "test:jbloggs", Email: "jbloggs@example.com", FullName: "Joe Bloggs", GravatarID: "62300f8842b68279680736dc1f9fc52e", IDPGroups: []string{}, PublicKeys: []*bakery.PublicKey{}, }, }, { about: "groups", userid: "test:jbloggs2", expectUser: ¶ms.User{ Username: "jbloggs2", ExternalID: "test:jbloggs2", Email: "jbloggs2@example.com", FullName: "Joe Bloggs II", GravatarID: "b1337cf8d58e2e2be9b6a5356cfc268b", IDPGroups: []string{ "test1", "test2", }, PublicKeys: []*bakery.PublicKey{}, }, }, { about: "no such user", userid: "test:not-there", expectError: `Get .*/v1/uid\?id=test%3Anot-there: identity "test:not-there" not found`, }} func (s *usersSuite) TestGetUserWithID(c *qt.C) { s.addUser(c, params.User{ Username: "jbloggs", ExternalID: "test:jbloggs", Email: "jbloggs@example.com", FullName: "Joe Bloggs", }) s.addUser(c, params.User{ Username: "jbloggs2", ExternalID: "test:jbloggs2", Email: "jbloggs2@example.com", FullName: "Joe Bloggs II", IDPGroups: []string{ "test1", "test2", }, }) for _, test := range getUserWithIDTests { c.Run(test.about, func(c *qt.C) { user, err := s.adminClient.GetUserWithID(s.srv.Ctx, ¶ms.GetUserWithIDRequest{ UserID: test.userid, }) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) return } c.Assert(err, qt.IsNil) c.Assert(user, qt.DeepEquals, test.expectUser) }) } } var getUserIDGroupsTests = []struct { about string userid string expectGroups []string expectError string }{{ about: "no groups", userid: "test:jbloggs", expectGroups: []string{}, }, { about: "groups", userid: "test:jbloggs2", expectGroups: []string{"test1", "test2"}, }, { about: "no such user", userid: "test:not-there", expectError: `Get .*/v1/uid/groups\?id=test%3Anot-there: identity "test:not-there" not found`, }} func (s *usersSuite) TestGetUserIDGroups(c *qt.C) { s.addUser(c, params.User{ Username: "jbloggs", ExternalID: "test:jbloggs", Email: "jbloggs@example.com", FullName: "Joe Bloggs", }) s.addUser(c, params.User{ Username: "jbloggs2", ExternalID: "test:jbloggs2", Email: "jbloggs2@example.com", FullName: "Joe Bloggs II", IDPGroups: []string{ "test1", "test2", }, }) for _, test := range getUserIDGroupsTests { c.Run(test.about, func(c *qt.C) { groups, err := s.adminClient.GetUserGroupsWithID(s.srv.Ctx, ¶ms.GetUserGroupsWithIDRequest{ UserID: test.userid, }) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) return } c.Assert(err, qt.IsNil) c.Assert(groups.Groups, qt.DeepEquals, test.expectGroups) }) } } golang-github-canonical-candid-1.12.3/jenkins/000077500000000000000000000000001457263123000211365ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/jenkins/jenkinsfiles/000077500000000000000000000000001457263123000236225ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/jenkins/jenkinsfiles/build-oci-image.jenkinsfile000066400000000000000000000061561457263123000310040ustar00rootroot00000000000000/* groovylint-disable NestedBlockDepth */ /* groovylint-disable-next-line CompileStatic, VariableTypeRequired, NoDef */ def oci pipeline { agent { // Not all of our slaves have multipass installed // TODO: Investigate why label 'slave-2' } options { // Some images rely on each other, each build should be run consecutively, // not concurrently disableConcurrentBuilds() // These kinds of logs and artifacts do not matter to us, // so, 10 should be fine for checking some history of the builds buildDiscarder(logRotator(numToKeepStr: '10')) // We always want the build to run fresh as vulnerabilities could have // been introduced at a later date disableResume() } environment { // Buildkit enables concurrent layer building for multi-stage builds // significantly speeding up build times DOCKER_BUILDKIT = 1 // Auth required for the go packages within the image GITHUB_PAT_AUTH = credentials('commercial-systems-bot') } parameters { string( name: 'http_proxy', defaultValue: 'http://squid.internal:3128', description: 'Enable access to HTTP via Squid proxy' ) } stages { stage('Setup environment') { steps { checkout scm script { try { // Load VM Utilities oci = load("${env.WORKSPACE}/jenkins/jenkinsfiles/utils/oci.groovy") } catch (err) { echo "error occured: ${err}" } } } } stage('Build image') { steps { script { try { oci.buildImage() } catch (err) { // TODO: Publish to mattermost or something echo "error occured: ${err}" } } echo 'Image built.' } } stage('Save image') { steps { script { try { oci.saveImage() } catch (err) { // TODO: Publish to mattermost or something echo "error occured: ${err}" } } echo 'Image saved.' } } stage('Scan image') { // We do not care for output right now, just to fail our pipeline // if N severities of J types are reached steps { script { try { oci.scanImage() } catch (err) { // TODO: Publish to mattermost or something echo "error occured: ${err}" } } } } } post { always { archiveArtifacts artifacts: 'candid-latest-image.tar.gz', sh "echo 'Image built'" } } } golang-github-canonical-candid-1.12.3/jenkins/jenkinsfiles/utils/000077500000000000000000000000001457263123000247625ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/jenkins/jenkinsfiles/utils/oci.groovy000066400000000000000000000021311457263123000270000ustar00rootroot00000000000000// Containers OCI related commands // for the build process. /** * Builds the dockerfile frontendv/0 compliant image. */ /* groovylint-disable-next-line BuilderMethodWithSideEffects, FactoryMethodName */ void buildImage() { sh """ docker build \ --build-arg http_proxy=${params.http_proxy} \ --build-arg https_proxy=${params.http_proxy} \ --secret id=ghuser,env=GITHUB_PAT_AUTH_USR \ --secret id=ghpat,env=GITHUB_PAT_AUTH_PSW \ . -f ./Dockerfile -t candid:latest """ } void saveImage() { sh """ docker save candid:latest | gzip > candid-latest-image.tar.gz """ } /** * Scans an image using trivvy. */ void scanImage() { sh """ docker run \ --env HTTP_PROXY=${params.http_proxy} \ --env HTTPS_PROXY=${params.http_proxy} \ --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v $HOME/Library/Caches:/root/.cache/ \ aquasec/trivy:0.31.3 image candid:latest """ } /* groovylint-disable-next-line CompileStatic */ return this golang-github-canonical-candid-1.12.3/meeting/000077500000000000000000000000001457263123000211255ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/meeting/client_generated.go000066400000000000000000000007401457263123000247510ustar00rootroot00000000000000// The code in this file was automatically generated by running httprequest-generate-client. // DO NOT EDIT package meeting import ( "context" "gopkg.in/httprequest.v1" ) type client struct { Client httprequest.Client } func (c *client) Done(ctx context.Context, p *doneRequest) error { return c.Client.Call(ctx, p, nil) } func (c *client) Wait(ctx context.Context, p *waitRequest) (*waitData, error) { var r *waitData err := c.Client.Call(ctx, p, &r) return r, err } golang-github-canonical-candid-1.12.3/meeting/export_test.go000066400000000000000000000005641457263123000240410ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package meeting // ItemCount reports the number of items stored locally in the Place. func ItemCount(p *Place) int { p.mu.Lock() defer p.mu.Unlock() return len(p.items) } var ( ReallyOldExpiryDuration = &reallyOldExpiryDuration RunGC = (*Place).runGC ) golang-github-canonical-candid-1.12.3/meeting/meeting.go000066400000000000000000000276141457263123000231160ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package meeting provides a way for one thread of control // to wait for information provided by another thread. package meeting import ( "context" "net" "net/http" "sync" "time" "github.com/juju/clock" "github.com/juju/loggo" "github.com/juju/utils/v2" "github.com/julienschmidt/httprouter" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "gopkg.in/tomb.v2" ) //go:generate httprequest-generate-client . handler client var logger = loggo.GetLogger("candid.meeting") var ( // pollInterval holds the interval at which the // garbage collector goroutine polls for expired // rendezvous. pollInterval = 30 * time.Second // defaultExpiryDuration holds the length of time that we keep // a rendezvous around before deleting it. This needs to // be long enough that the user can do all the web page // interaction that they need to before the rendezvous is // completed. defaultExpiryDuration = time.Hour // defaultWaitTimeout holds the default maximum // length of time that a wait request can block for. defaultWaitTimeout = time.Minute // reallyOldExpiryDuration holds the length of time after // which we'll delete rendezvous regardless of server. // This caters for the case where a given server has restarted // without removing its existing entries. reallyOldExpiryDuration = 7 * 24 * time.Hour // Clock holds the clock implementation used by the meeting package. // This is exported so it can be changed for testing purposes. Clock clock.Clock = clock.WallClock ) // Store defines the backing store required by the // participants in the rendezvous. // Entries created in the store should be visible // to all participants. type Store interface { // Context returns a context that is suitable for passing to the // other store methods. Store methods called with such a context // will be sequentially consistent; for example, a value that is // Put will immediately be available from a Get method. // // The returned close function must be called when the returned // context will no longer be used, to allow for any required // cleanup. Context(ctx context.Context) (_ context.Context, close func()) // Put associates an address with the given id. Put(ctx context.Context, id, address string) error // Get returns the address associated with the given id // and removes the association. Get(ctx context.Context, id string) (address string, err error) // Remove removes the entry with the given id. // It should not return an error if the entry has already // been removed. Remove(ctx context.Context, id string) (time.Time, error) // RemoveOld removes entries with the given address that were created // earlier than the given time. It returns any ids removed. // If it encountered an error while deleting the ids, it // may return a non-empty ids slice and a non-nil error. RemoveOld(ctx context.Context, address string, olderThan time.Time) (ids []string, err error) } // Place represents a rendezvous place. type Place struct { tomb tomb.Tomb store Store localAddr string listener net.Listener handler *handler metrics Metrics waitTimeout time.Duration expiryDuration time.Duration mu sync.Mutex items map[string]*item } type item struct { created time.Time c chan struct{} data0 []byte data1 []byte } // Metrics represents a way to report metrics information // about the meeting service. It must be callable // concurrently. type Metrics interface { // RequestCompleted is called every time an HTTP // request has completed with the time the request started. RequestCompleted(startTime time.Time) // RequestsExpired is called when some requests // have been garbage collected with the number // of GC'd requests. RequestsExpired(count int) } // Params holds parameters for the NewServer function. type Params struct { // Store is used for storage of persistent data. Store Store // Metrics holds an object that's used to report server metrics. // If it's nil, no metrics will be reported. Metrics Metrics // ListenAddr holds the host name to listen on. This // should not have a port number. // Note that ListenAddr must also be sufficient for other // servers to use to contact this one. ListenAddr string // DisableGC holds whether the garbage collector is disabled. DisableGC bool // WaitTimeout holds the maximum time to that // wait requests will wait. If it is zero, a default // duration will be used. WaitTimeout time.Duration // ExpiryDuration holds the maximum amount of time // a rendezvous will be kept around for. If it is zero, a default // duration will be used. ExpiryDuration time.Duration } // NewServer returns a new rendezvous place using the given // parameters. func NewPlace(params Params) (*Place, error) { listener, err := net.Listen("tcp", net.JoinHostPort(params.ListenAddr, "0")) if err != nil { return nil, errgo.Notef(err, "cannot start listener") } if params.Metrics == nil { params.Metrics = noMetrics{} } if params.WaitTimeout == 0 { params.WaitTimeout = defaultWaitTimeout } if params.ExpiryDuration == 0 { params.ExpiryDuration = defaultExpiryDuration } p := &Place{ store: params.Store, listener: listener, localAddr: listener.Addr().String(), items: make(map[string]*item), metrics: params.Metrics, waitTimeout: params.WaitTimeout, expiryDuration: params.ExpiryDuration, } p.handler = &handler{ place: p, } router := httprouter.New() for _, h := range reqServer.Handlers(p.newHandler) { router.Handle(h.Method, h.Path, h.Handle) } if !params.DisableGC { p.tomb.Go(p.gc) } p.tomb.Go(func() error { http.Serve(p.listener, router) return nil }) return p, nil } // Close shuts down the rendezvous place. func (p *Place) Close() { p.listener.Close() p.tomb.Kill(nil) p.tomb.Wait() } // gc garbage collects expired rendezvous by polling occasionally. func (p *Place) gc() error { dying := false for { ctx, close := p.store.Context(context.Background()) err := p.runGC(ctx, dying, Clock.Now()) close() if err != nil { logger.Errorf("meeting GC: %v", err) } if dying { return nil } // We wait at the end of the loop rather than the start // so we are always guaranteed a GC when the server starts // up. select { case <-Clock.After(pollInterval): case <-p.tomb.Dying(): dying = true } } } // runGC runs a single garbage collection at the given time. // If dying is true, it removes all entries in the server. func (p *Place) runGC(ctx context.Context, dying bool, now time.Time) error { var expiryTime time.Time if dying { // A little bit in the future so that we're sure to // find all entries. expiryTime = now.Add(time.Millisecond) } else { expiryTime = now.Add(-p.expiryDuration) } ids, err := p.store.RemoveOld(ctx, p.localAddr, expiryTime) if len(ids) > 0 { p.mu.Lock() for _, id := range ids { delete(p.items, id) } p.mu.Unlock() p.metrics.RequestsExpired(len(ids)) } if err != nil { return errgo.Notef(err, "cannot remove old entries") } ids, err = p.store.RemoveOld(ctx, "", now.Add(-reallyOldExpiryDuration)) if err != nil { return errgo.Notef(err, "cannot remove really old entries") } if len(ids) > 0 { p.metrics.RequestsExpired(len(ids)) } return nil } // localWait is the internal version of Place.Wait. // It only works if the given id is stored locally. func (p *Place) localWait(ctx context.Context, id string) (data0, data1 []byte, err error) { logger.Infof("localWait %q", id) p.mu.Lock() item := p.items[id] p.mu.Unlock() if item == nil { return nil, nil, errgo.Newf("rendezvous %q not found", id) } now := Clock.Now() expiryDeadline := item.created.Add(p.expiryDuration) deadline := expiryDeadline if t := now.Add(p.waitTimeout); t.Before(deadline) { deadline = t } logger.Infof("timeout %v", deadline.Sub(now)) ctx, cancel := utils.ContextWithTimeout(ctx, Clock, deadline.Sub(now)) defer cancel() // Wait for the channel to be closed by Done or for the overall // expiry deadline or the wait to pass, whichever comes first. var expiredErr error select { case <-item.c: case <-ctx.Done(): expiredErr = ctx.Err() } removed := false if expiredErr == nil || Clock.Now().After(expiryDeadline) { // The client has acquired the rendezvous OK or the full // expiry duration has elapsed, so remove the item. Note // that we're getting the Store *after* waiting, so we // don't tie up resources while waiting. ctx, close := p.store.Context(ctx) defer close() p.mu.Lock() defer p.mu.Unlock() delete(p.items, id) _, err := p.store.Remove(ctx, id) if err != nil { logger.Errorf("cannot remove rendezvous %q: %v", id, err) } removed = true } if expiredErr != nil { if removed { return nil, nil, errgo.Newf("rendezvous expired after %v", p.expiryDuration) } return nil, nil, errgo.Notef(err, "rendezvous wait timed out") } // TODO what do we actually want RequestCompleted to signify? p.metrics.RequestCompleted(item.created) return item.data0, item.data1, nil } // localDone is the internal version of Place.Done. // It only works if the given id is stored locally. func (p *Place) localDone(id string, data []byte) error { p.mu.Lock() defer p.mu.Unlock() item := p.items[id] if item == nil { return errgo.Newf("rendezvous %q not found", id) } select { case <-item.c: return errgo.Newf("rendezvous %q done twice", id) default: item.data1 = data close(item.c) } return nil } func (p *Place) isLocal(id string) bool { p.mu.Lock() defer p.mu.Unlock() return p.items[id] != nil } func (p *Place) newHandler(params httprequest.Params) (*handler, context.Context, error) { return p.handler, params.Context, nil } var reqServer = httprequest.Server{ ErrorMapper: func(ctx context.Context, err error) (httpStatus int, errorBody interface{}) { return http.StatusInternalServerError, &httprequest.RemoteError{ Message: err.Error(), } }, } // NewRendezvous creates a new rendezvous holding // the given data. The rendezvous id is returned. func (p *Place) NewRendezvous(ctx context.Context, id string, data []byte) error { p.mu.Lock() p.items[id] = &item{ created: Clock.Now(), c: make(chan struct{}), data0: data, } p.mu.Unlock() if err := p.store.Put(ctx, id, p.localAddr); err != nil { p.mu.Lock() defer p.mu.Unlock() delete(p.items, id) return errgo.Notef(err, "cannot create entry for rendezvous") } return nil } // Wait waits for the rendezvous with the given id // and returns the data provided to NewRendezvous // and the data provided to Done. func (p *Place) Wait(ctx context.Context, id string) (data0, data1 []byte, err error) { logger.Infof("Wait %q", id) if p.isLocal(id) { return p.localWait(ctx, id) } logger.Infof("not local wait") client, err := p.clientForId(ctx, id) if err != nil { return nil, nil, errgo.Mask(err) } resp, err := client.Wait(ctx, &waitRequest{ Id: id, }) if err != nil { return nil, nil, errgo.Mask(err) } return resp.Data0, resp.Data1, nil } // Done marks the rendezvous with the given id as complete, // and provides it with the given data which will be // returned from Wait. func (p *Place) Done(ctx context.Context, id string, data []byte) error { if p.isLocal(id) { return p.localDone(id, data) } client, err := p.clientForId(ctx, id) if err != nil { return errgo.Mask(err) } if err := client.Done(ctx, &doneRequest{ Id: id, Body: doneData{ Data1: data, }, }); err != nil { return errgo.Mask(err) } return nil } func (p *Place) clientForId(ctx context.Context, id string) (*client, error) { addr, err := p.store.Get(ctx, id) if err != nil { return nil, errgo.Mask(err) } return &client{ Client: httprequest.Client{ BaseURL: "http://" + addr, }, }, nil } // noMetrics implements Metrics by doing nothing. type noMetrics struct{} func (noMetrics) RequestCompleted(startTime time.Time) {} func (noMetrics) RequestsExpired(count int) {} golang-github-canonical-candid-1.12.3/meeting/meeting_test.go000066400000000000000000000406271457263123000241540ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package meeting_test import ( "context" "crypto/rand" "fmt" "sync" "sync/atomic" "testing" "time" qt "github.com/frankban/quicktest" "github.com/juju/clock" "github.com/juju/clock/testclock" "gopkg.in/errgo.v1" "github.com/canonical/candid/meeting" ) var epoch = parseTime("2016-01-01T12:00:00Z") func parseTime(s string) time.Time { t, err := time.Parse(time.RFC3339, s) if err != nil { panic(err) } return t } type nilMetrics struct{} func (nilMetrics) RequestCompleted(startTime time.Time) {} func (nilMetrics) RequestsExpired(count int) {} func TestRendezvousWaitBeforeDone(t *testing.T) { c := qt.New(t) defer c.Done() clock := testclock.NewClock(epoch) c.Patch(&meeting.Clock, clock) count := int32(0) store := newFakeStore(&count, clock) m, err := meeting.NewPlace(meeting.Params{ Store: store, ListenAddr: "localhost", DisableGC: true, }) c.Assert(err, qt.IsNil) defer m.Close() ctx := context.Background() id, err := newId() c.Assert(err, qt.IsNil) err = m.NewRendezvous(ctx, id, []byte("first data")) c.Assert(id, qt.Not(qt.Equals), "") waitDone := make(chan struct{}) go func() { data0, data1, err := m.Wait(ctx, id) c.Check(err, qt.Equals, nil) c.Check(string(data0), qt.Equals, "first data") c.Check(string(data1), qt.Equals, "second data") close(waitDone) }() clock.Advance(10 * time.Millisecond) err = m.Done(ctx, id, []byte("second data")) c.Assert(err, qt.IsNil) select { case <-waitDone: case <-time.After(2 * time.Second): c.Errorf("timed out waiting for rendezvous") } // Check that item has now been deleted. data0, data1, err := m.Wait(ctx, id) c.Assert(data0, qt.IsNil) c.Assert(data1, qt.IsNil) c.Assert(err, qt.ErrorMatches, `rendezvous ".*" not found`) c.Assert(atomic.LoadInt32(&count), qt.Equals, int32(0)) } func TestRendezvousDoneBeforeWait(t *testing.T) { c := qt.New(t) defer c.Done() clock := testclock.NewClock(epoch) c.Patch(&meeting.Clock, clock) count := int32(0) store := newFakeStore(&count, clock) p, err := meeting.NewPlace(meeting.Params{ Store: store, ListenAddr: "localhost", DisableGC: true, }) c.Assert(err, qt.IsNil) defer p.Close() ctx := context.Background() id, err := newId() c.Assert(err, qt.IsNil) err = p.NewRendezvous(ctx, id, []byte("first data")) c.Assert(err, qt.IsNil) c.Assert(id, qt.Not(qt.Equals), "") err = p.Done(ctx, id, []byte("second data")) c.Assert(err, qt.IsNil) err = p.Done(ctx, id, []byte("other second data")) c.Assert(err, qt.ErrorMatches, `.*rendezvous ".*" done twice`) data0, data1, err := p.Wait(ctx, id) c.Assert(err, qt.IsNil) c.Assert(string(data0), qt.Equals, "first data") c.Assert(string(data1), qt.Equals, "second data") // Check that item has now been deleted. data0, data1, err = p.Wait(ctx, id) c.Assert(data0, qt.IsNil) c.Assert(data1, qt.IsNil) c.Assert(err, qt.ErrorMatches, `rendezvous ".*" not found`) c.Assert(atomic.LoadInt32(&count), qt.Equals, int32(0)) } func TestRendezvousDifferentPlaces(t *testing.T) { c := qt.New(t) defer c.Done() clock := testclock.NewClock(epoch) c.Patch(&meeting.Clock, clock) count := int32(0) store := newFakeStore(&count, clock) m1, err := meeting.NewPlace(meeting.Params{ Store: store, ListenAddr: "localhost", DisableGC: true, }) c.Assert(err, qt.IsNil) defer m1.Close() m2, err := meeting.NewPlace(meeting.Params{ Store: store, ListenAddr: "localhost", }) c.Assert(err, qt.IsNil) defer m2.Close() m3, err := meeting.NewPlace(meeting.Params{ Store: store, ListenAddr: "localhost", }) c.Assert(err, qt.IsNil) defer m3.Close() ctx := context.Background() // Create the rendezvous in m1. id, err := newId() c.Assert(err, qt.IsNil) err = m1.NewRendezvous(ctx, id, []byte("first data")) c.Assert(err, qt.IsNil) c.Assert(id, qt.Not(qt.Equals), "") // Wait for the rendezvous in m2. waitDone := make(chan struct{}) go func() { data0, data1, err := m2.Wait(ctx, id) c.Check(err, qt.Equals, nil) c.Check(string(data0), qt.Equals, "first data") c.Check(string(data1), qt.Equals, "second data") close(waitDone) }() clock.Advance(10 * time.Millisecond) err = m3.Done(ctx, id, []byte("second data")) c.Assert(err, qt.IsNil) select { case <-waitDone: case <-time.After(2 * time.Second): c.Errorf("timed out waiting for rendezvous") } // Check that item has now been deleted. data0, data1, err := m3.Wait(ctx, id) c.Assert(data0, qt.IsNil) c.Assert(data1, qt.IsNil) c.Assert(err, qt.ErrorMatches, `rendezvous ".*" not found`) c.Assert(atomic.LoadInt32(&count), qt.Equals, int32(0)) } func TestEntriesRemovedOnClose(t *testing.T) { c := qt.New(t) defer c.Done() clock := testclock.NewClock(epoch) c.Patch(&meeting.Clock, clock) store := newFakeStore(nil, clock) m1, err := meeting.NewPlace(meeting.Params{ Store: store, ListenAddr: "localhost", }) c.Assert(err, qt.IsNil) m2, err := meeting.NewPlace(meeting.Params{ Store: store, ListenAddr: "localhost", }) c.Assert(err, qt.IsNil) ctx := context.Background() for i := 0; i < 3; i++ { err := m1.NewRendezvous(ctx, fmt.Sprintf("1%04x", i), []byte("something")) c.Assert(err, qt.IsNil) } for i := 0; i < 5; i++ { err := m2.NewRendezvous(ctx, fmt.Sprintf("2%04x", i), []byte("something")) c.Assert(err, qt.IsNil) } m1.Close() c.Assert(meeting.ItemCount(m1), qt.Equals, 0) c.Assert(store.itemCount(), qt.Equals, 5) m2.Close() c.Assert(store.itemCount(), qt.Equals, 0) } func TestRunGCNotDying(t *testing.T) { c := qt.New(t) defer c.Done() const expiryDuration = time.Hour clock := testclock.NewClock(epoch) store := newFakeStore(nil, clock) m1, err := meeting.NewPlace(meeting.Params{ Store: store, ListenAddr: "localhost", ExpiryDuration: expiryDuration, DisableGC: true, }) c.Assert(err, qt.IsNil) m2, err := meeting.NewPlace(meeting.Params{ Store: store, ListenAddr: "localhost", ExpiryDuration: expiryDuration, DisableGC: true, }) c.Assert(err, qt.IsNil) ctx := context.Background() var ids1, ids2 []string now := time.Now() // Create four rendezvous using the both servers server, // one really old, two old and one newer. for _, d := range []time.Duration{ *meeting.ReallyOldExpiryDuration + time.Millisecond, expiryDuration + time.Millisecond, expiryDuration + 2*time.Millisecond, expiryDuration / 2, } { id, err := newId() c.Assert(err, qt.IsNil) err = m1.NewRendezvous(ctx, id, []byte("something")) c.Assert(err, qt.IsNil) ids1 = append(ids1, id) store.setCreationTime(id, now.Add(-d)) id, err = newId() c.Assert(err, qt.IsNil) err = m2.NewRendezvous(ctx, id, []byte("something")) c.Assert(err, qt.IsNil) ids2 = append(ids2, id) store.setCreationTime(id, now.Add(-d)) } err = meeting.RunGC(m1, ctx, false, now) c.Assert(err, qt.IsNil) // All the expired ids on the server we ran the GC on should have // been collected. for i, id := range ids1[0:3] { err := m1.Done(ctx, id, nil) c.Assert(err, qt.ErrorMatches, `rendezvous ".*" not found`, qt.Commentf("id %d", i)) } // The unexpired one should still be around. err = m1.Done(ctx, ids1[3], nil) c.Assert(err, qt.IsNil) // The really old id on the other server should have been collected. err = m1.Done(ctx, ids2[0], nil) c.Assert(err, qt.ErrorMatches, `rendezvous ".*" not found`) // All the others should still be around. for _, id := range ids2[1:] { err = m1.Done(ctx, id, nil) c.Assert(err, qt.IsNil) } } func TestPartialRemoveOldFailure(t *testing.T) { c := qt.New(t) defer c.Done() clock := testclock.NewClock(epoch) const expiryDuration = time.Hour // RemoveOld can fail with ids and an error. If it // does so, the database and the server should remain // consistent. store := partialRemoveStore{newFakeStore(nil, clock)} m, err := meeting.NewPlace(meeting.Params{ Store: store, ListenAddr: "localhost", ExpiryDuration: expiryDuration, DisableGC: true, }) c.Assert(err, qt.IsNil) ctx := context.Background() now := time.Now() for _, d := range []time.Duration{ expiryDuration + time.Millisecond, expiryDuration + 2*time.Millisecond, expiryDuration / 2, } { id, err := newId() c.Assert(err, qt.IsNil) err = m.NewRendezvous(ctx, id, []byte("something")) c.Assert(err, qt.IsNil) store.setCreationTime(id, now.Add(-d)) } err = meeting.RunGC(m, ctx, false, now) c.Assert(err, qt.ErrorMatches, "cannot remove old entries: partial error") c.Assert(meeting.ItemCount(m), qt.Equals, 2) c.Assert(store.itemCount(), qt.Equals, 2) err = meeting.RunGC(m, ctx, false, now) c.Assert(err, qt.ErrorMatches, "cannot remove old entries: partial error") c.Assert(meeting.ItemCount(m), qt.Equals, 1) c.Assert(store.itemCount(), qt.Equals, 1) } type partialRemoveStore struct { *fakeStore } func (s partialRemoveStore) RemoveOld(ctx context.Context, addr string, olderThan time.Time) ([]string, error) { s.mu.Lock() defer s.mu.Unlock() for id, entry := range s.entries { if entry.creationTime.Before(olderThan) && (addr == "" || entry.addr == addr) { delete(s.entries, id) return []string{id}, errgo.New("partial error") } } return nil, nil } func TestPutFailure(t *testing.T) { c := qt.New(t) store := putErrorStore{} m, err := meeting.NewPlace(meeting.Params{ Store: store, ListenAddr: "localhost", DisableGC: true, }) c.Assert(err, qt.IsNil) defer m.Close() ctx := context.Background() id, err := newId() c.Assert(err, qt.IsNil) err = m.NewRendezvous(ctx, id, []byte("x")) c.Assert(err, qt.ErrorMatches, "cannot create entry for rendezvous: put error") c.Assert(meeting.ItemCount(m), qt.Equals, 0) } func TestWaitTimeout(t *testing.T) { c := qt.New(t) defer c.Done() ctx := context.Background() clock := testclock.NewClock(epoch) store := newFakeStore(nil, clock) c.Patch(&meeting.Clock, clock) params := meeting.Params{ Store: store, ListenAddr: "localhost", DisableGC: true, WaitTimeout: time.Second, ExpiryDuration: 5 * time.Second, } m, err := meeting.NewPlace(params) c.Assert(err, qt.IsNil) t0 := clock.Now() id, err := newId() c.Assert(err, qt.IsNil) err = m.NewRendezvous(ctx, id, nil) c.Assert(err, qt.IsNil) done := make(chan struct{}) go func() { c.Logf("starting wait %q", id) _, _, err := m.Wait(ctx, id) c.Check(err, qt.ErrorMatches, "rendezvous wait timed out") done <- struct{}{} }() err = clock.WaitAdvance(params.WaitTimeout+1, time.Second, 1) c.Assert(err, qt.IsNil) select { case <-done: case <-time.After(time.Second): c.Fatalf("timed out waiting for Wait to time out") } // Try again. The item shouldn't have been removed, so we should be // able to repeat the request. go func() { _, _, err := m.Wait(ctx, id) c.Check(err, qt.ErrorMatches, "rendezvous wait timed out") done <- struct{}{} }() err = clock.WaitAdvance(params.WaitTimeout+1, time.Second, 1) c.Assert(err, qt.IsNil) select { case <-done: case <-time.After(time.Second): c.Fatalf("timed out waiting for Wait to time out") } c.Logf("after second wait, now: %v", clock.Now()) // When the actual expiry deadline passes while we're waiting, // we should return when that happens. // Advance the clock to just before the expiry duration. expiryDeadline := t0.Add(params.ExpiryDuration) c.Logf("expiry deadline %v", expiryDeadline) clock.Advance(expiryDeadline.Add(-time.Millisecond).Sub(clock.Now())) go func() { _, _, err := m.Wait(ctx, id) c.Check(err, qt.ErrorMatches, "rendezvous expired after 5s") done <- struct{}{} }() waitDuration := expiryDeadline.Add(1).Sub(clock.Now()) c.Logf("final wait from %v: %v", clock.Now(), waitDuration) err = clock.WaitAdvance(expiryDeadline.Add(1).Sub(clock.Now()), time.Second, 1) c.Assert(err, qt.IsNil) c.Logf("final time %v", clock.Now()) select { case <-done: case <-time.After(time.Second): c.Fatalf("timed out waiting for Wait to time out") } } func TestRequestCompletedCalled(t *testing.T) { c := qt.New(t) defer c.Done() clock := testclock.NewClock(epoch) c.Patch(&meeting.Clock, clock) store := newFakeStore(nil, clock) tm := newTestMetrics() m, err := meeting.NewPlace(meeting.Params{ Store: store, Metrics: tm, ListenAddr: "localhost", }) c.Assert(err, qt.IsNil) defer m.Close() ctx := context.Background() id, err := newId() c.Assert(err, qt.IsNil) err = m.NewRendezvous(ctx, id, nil) c.Assert(err, qt.IsNil) c.Assert(id, qt.Not(qt.Equals), "") waitDone := make(chan struct{}) go func() { _, _, err := m.Wait(ctx, id) c.Check(err, qt.Equals, nil) c.Check(tm.completedCallCount, qt.Equals, 1) close(waitDone) }() clock.Advance(10 * time.Millisecond) err = m.Done(ctx, id, nil) c.Assert(err, qt.IsNil) select { case <-waitDone: case <-time.After(2 * time.Second): c.Errorf("timed out waiting for rendezvous") } // Check that item has now been deleted. _, _, err = m.Wait(ctx, id) c.Assert(err, qt.ErrorMatches, `rendezvous ".*" not found`) } func TestRequestsExpiredCalled(t *testing.T) { c := qt.New(t) defer c.Done() clock := testclock.NewClock(epoch) c.Patch(&meeting.Clock, clock) store := newFakeStore(nil, clock) tm := newTestMetrics() m, err := meeting.NewPlace(meeting.Params{ Store: store, Metrics: tm, ListenAddr: "localhost", }) c.Assert(err, qt.IsNil) ctx := context.Background() for i := 0; i < 3; i++ { err := m.NewRendezvous(ctx, fmt.Sprintf("%04x", i), nil) c.Assert(err, qt.IsNil) } m.Close() c.Assert(tm.expiredCallCount, qt.Equals, 1) c.Assert(tm.expiredCallValues, qt.DeepEquals, []int{3}) } type testMetrics struct { completedCallCount int expiredCallCount int expiredCallValues []int } func newTestMetrics() *testMetrics { return &testMetrics{ expiredCallValues: []int{}, } } func (m *testMetrics) RequestCompleted(startTime time.Time) { m.completedCallCount++ } func (m *testMetrics) RequestsExpired(count int) { m.expiredCallCount++ m.expiredCallValues = append(m.expiredCallValues, count) } type putErrorStore struct { meeting.Store } func (putErrorStore) Put(_ context.Context, id, address string) error { return errgo.Newf("put error") } func (putErrorStore) RemoveOld(context.Context, string, time.Time) ([]string, error) { return nil, nil } type fakeStore struct { clock clock.Clock count *int32 mu sync.Mutex entries map[string]*fakeStoreEntry } type fakeStoreEntry struct { addr string creationTime time.Time } // newFakeStore returns an in memory store implementation. // If count is non-nil, will atomically decrement it whenever the // store is closed. func newFakeStore(count *int32, clck clock.Clock) *fakeStore { if count == nil { count = new(int32) } if clck == nil { clck = clock.WallClock } return &fakeStore{ clock: clck, count: count, entries: make(map[string]*fakeStoreEntry), } } func (s *fakeStore) itemCount() int { s.mu.Lock() defer s.mu.Unlock() return len(s.entries) } func (s *fakeStore) setCreationTime(id string, t time.Time) { s.mu.Lock() defer s.mu.Unlock() s.entries[id].creationTime = t } // Context implements Store.Context. func (s *fakeStore) Context(ctx context.Context) (_ context.Context, close func()) { atomic.AddInt32(s.count, 1) return ctx, func() { atomic.AddInt32(s.count, -1) } } // Put implements Store.Put. func (s *fakeStore) Put(_ context.Context, id, addr string) error { s.mu.Lock() defer s.mu.Unlock() s.entries[id] = &fakeStoreEntry{ addr: addr, creationTime: s.clock.Now(), } return nil } // Get implements Store.Get. func (s *fakeStore) Get(_ context.Context, id string) (address string, err error) { s.mu.Lock() defer s.mu.Unlock() if entry := s.entries[id]; entry != nil { return entry.addr, nil } return "", errgo.Newf("rendezvous %q not found", id) } // Remove implements Store.Remove. func (s *fakeStore) Remove(_ context.Context, id string) (time.Time, error) { s.mu.Lock() defer s.mu.Unlock() delete(s.entries, id) return epoch, nil } // RemoveOld implements Store.RemoveOld. func (s *fakeStore) RemoveOld(_ context.Context, addr string, olderThan time.Time) (ids []string, err error) { s.mu.Lock() defer s.mu.Unlock() for id, entry := range s.entries { if entry.creationTime.Before(olderThan) && (addr == "" || entry.addr == addr) { delete(s.entries, id) ids = append(ids, id) } } return ids, nil } func newId() (string, error) { var id [16]byte if _, err := rand.Read(id[:]); err != nil { return "", errgo.Notef(err, "cannot read random id") } return fmt.Sprintf("%x", id[:]), nil } golang-github-canonical-candid-1.12.3/meeting/server.go000066400000000000000000000017631457263123000227710ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package meeting import ( "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" ) type handler struct { place *Place } type waitRequest struct { httprequest.Route `httprequest:"GET /:Id"` Id string `httprequest:",path"` } type waitData struct { Data0 []byte Data1 []byte } func (h *handler) Wait(p httprequest.Params, req *waitRequest) (*waitData, error) { data0, data1, err := h.place.localWait(p.Context, req.Id) if err != nil { return nil, errgo.Mask(err) } return &waitData{ Data0: data0, Data1: data1, }, nil } type doneData struct { Data1 []byte } type doneRequest struct { httprequest.Route `httprequest:"PUT /:Id"` Id string `httprequest:",path"` Body doneData `httprequest:",body"` } func (h *handler) Done(req *doneRequest) error { if err := h.place.localDone(req.Id, req.Body.Data1); err != nil { return errgo.Mask(err) } return nil } golang-github-canonical-candid-1.12.3/params/000077500000000000000000000000001457263123000207605ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/params/error.go000066400000000000000000000031631457263123000224430ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package params import ( "fmt" ) // ErrorCode holds the class of an error in machine-readable format. // It is also an error in its own right. type ErrorCode string func (code ErrorCode) Error() string { return string(code) } func (code ErrorCode) ErrorCode() ErrorCode { return code } const ( ErrNotFound ErrorCode = "not found" ErrForbidden ErrorCode = "forbidden" ErrBadRequest ErrorCode = "bad request" ErrUnauthorized ErrorCode = "unauthorized" ErrAlreadyExists ErrorCode = "already exists" ErrNoAdminCredsProvided ErrorCode = "no admin credentials provided" ErrMethodNotAllowed ErrorCode = "method not allowed" ErrServiceUnavailable ErrorCode = "service unavailable" ErrInternalServer ErrorCode = "internal server error" ) // Error represents an error - it is returned for any response that fails. type Error struct { Message string `json:"message,omitempty"` Code ErrorCode `json:"code,omitempty"` } // NewError returns a new *Error with the given error code // and message. func NewError(code ErrorCode, f string, a ...interface{}) error { return &Error{ Message: fmt.Sprintf(f, a...), Code: code, } } // Error implements error.Error. func (e *Error) Error() string { return e.Message } // ErrorCode holds the class of the error in machine readable format. func (e *Error) ErrorCode() string { return e.Code.Error() } // Cause implements errgo.Causer.Cause. func (e *Error) Cause() error { if e.Code != "" { return e.Code } return nil } golang-github-canonical-candid-1.12.3/params/params.go000066400000000000000000000315771457263123000226070ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package params import ( "time" "unicode/utf8" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "gopkg.in/macaroon.v2" ) // Username represents the name of a user. type Username string // UnmarshalText unmarshals a Username checking it is valid. It // implements "encoding".TextUnmarshaler. func (u *Username) UnmarshalText(b []byte) error { if utf8.RuneCount(b) > 256 { return errgo.New("username longer than 256 characters") } *u = Username(string(b)) return nil } // AgentLogin contains the claimed identity the agent is attempting to // use to log in. type AgentLogin struct { Username Username `json:"username"` PublicKey *bakery.PublicKey `json:"public_key"` } // AgentLoginResponse contains the response to an agent login attempt. type AgentLoginResponse struct { AgentLogin bool `json:"agent_login"` } // PublicKeyRequest documents the /publickey endpoint. As // it contains no request information there is no need to ever create // one. type PublicKeyRequest struct { httprequest.Route `httprequest:"GET /publickey"` } // PublicKeyResponse is the response to a PublicKeyRequest. type PublicKeyResponse struct { PublicKey *bakery.PublicKey } // LoginMethods holds the response from the /login endpoint // when called with "Accept: application/json". This enumerates // the available methods for the client to log in. type LoginMethods struct { // Agent is the endpoint to connect to, if the client wishes to // authenticate as an agent. Agent string `json:"agent,omitempty"` // Interactive is the endpoint to connect to, if the user can // interact with the login process. Interactive string `json:"interactive,omitempty"` // UbuntuSSOOAuth is the endpoint to send a request, signed with // UbuntuSSO OAuth credentials, to if the client wishes to use // oauth to log in to Identity Manager. Ubuntu SSO uses oauth 1.0. UbuntuSSOOAuth string `json:"usso_oauth,omitempty"` // UbuntuSSODischarge allows login to be performed by discharging // a macaroon with a third-party caveat addressed to Ubuntu SSO. UbuntuSSODischarge string `json:"usso_discharge,omitempty"` // Form is the endpoint to GET a schema for a login form which // can be presented to the user in an interactive manner. The // schema will be returned as an environschema.Fields object. The // completed form should be POSTed back to the same endpoint. Form string `json:"form,omitempty"` } // QueryUsersRequest is a request to query the users in the system. type QueryUsersRequest struct { httprequest.Route `httprequest:"GET /v1/u"` // ExternalID, if present, matches all identities with the given // external ID (there should be a maximum of 1). ExternalID string `httprequest:"external_id,form"` // EMail, if present, matches all identities with the given email // address. Email string `httprequest:"email,form"` // LastLoginSince, if present, must contain a time marshaled as // if using Time.MarshalText. It matches all identies that have a // last login time after the given time. LastLoginSince string `httprequest:"last-login-since,form"` // LastDischargeSince, if present, must contain a time marshaled as // if using Time.MarshalText. It matches all identies that have a // last discharge time after the given time. LastDischargeSince string `httprequest:"last-discharge-since,form"` // Owner, if present, matches all agent identities with the given // owner. Owner string `httprequest:"owner,form"` } // UserRequest is a request for the user details of the named user. type UserRequest struct { httprequest.Route `httprequest:"GET /v1/u/:username"` Username Username `httprequest:"username,path"` } // User represents a user in the system. type User struct { Username Username `json:"username,omitempty"` ExternalID string `json:"external_id"` FullName string `json:"fullname"` Email string `json:"email"` GravatarID string `json:"gravatar_id"` IDPGroups []string `json:"idpgroups"` Owner Username `json:"owner,omitempty"` PublicKeys []*bakery.PublicKey `json:"public_keys"` SSHKeys []string `json:"ssh_keys"` LastLogin *time.Time `json:"last_login,omitempty"` LastDischarge *time.Time `json:"last_discharge,omitempty"` } // SetUserRequest is a request to set the details of a user. // This endpoint is no longer functional. type SetUserRequest struct { httprequest.Route `httprequest:"PUT /v1/u/:username"` Username Username `httprequest:"username,path"` User `httprequest:",body"` } // CreateAgentRequest is a request to add an agent. type CreateAgentRequest struct { httprequest.Route `httprequest:"POST /v1/u"` CreateAgentBody `httprequest:",body"` } // CreateAgentBody holds the body of a CreateAgentRequest. // There must be at least one public key specified. type CreateAgentBody struct { FullName string `json:"fullname"` Groups []string `json:"idpgroups"` PublicKeys []*bakery.PublicKey `json:"public_keys"` // A parent agent is one that can create its own agents. A parent // agent does not have an owner and so remains a member of the // groups it has been allocated irrespective of whether the // creating user remains a member. Only users in the write-user // ACL can create a parent agent. Parent bool `json:"parent,omitempty"` } // CreateAgentResponse holds the response from a // CreateAgentRequest. type CreateAgentResponse struct { Username Username } // UserGroupsRequest is a request for the list of groups associated // with the specified user. type UserGroupsRequest struct { httprequest.Route `httprequest:"GET /v1/u/:username/groups"` Username Username `httprequest:"username,path"` } // SetUserGroupsRequest is a request to set the list of groups associated // with the specified user. type SetUserGroupsRequest struct { httprequest.Route `httprequest:"PUT /v1/u/:username/groups"` Username Username `httprequest:"username,path"` Groups Groups `httprequest:",body"` } // Groups contains a list of group names. type Groups struct { Groups []string `json:"groups"` } // ModifyUserGroupsRequest is a request to update the list of groups associated // with the specified user. type ModifyUserGroupsRequest struct { httprequest.Route `httprequest:"POST /v1/u/:username/groups"` Username Username `httprequest:"username,path"` Groups ModifyGroups `httprequest:",body"` } // ModifyGroups contains a set of group list modifications. type ModifyGroups struct { Add []string `json:"add"` Remove []string `json:"remove"` } // UserIDPGroupsRequest defines the deprecated path for // UserGroupsRequest. It should no longer be used. type UserIDPGroupsRequest struct { httprequest.Route `httprequest:"GET /v1/u/:username/idpgroups"` UserGroupsRequest } // UserTokenRequest is a request for a new token to represent the user. type UserTokenRequest struct { httprequest.Route `httprequest:"GET /v1/u/:username/macaroon"` Username Username `httprequest:"username,path"` } // VerifyTokenRequest is a request to verify that the provided // macaroon.Slice is valid and represents a user from identity. type VerifyTokenRequest struct { httprequest.Route `httprequest:"POST /v1/verify"` Macaroons macaroon.Slice `httprequest:",body"` } // SSHKeysRequest is a request for the list of ssh keys associated // with the specified user. type SSHKeysRequest struct { httprequest.Route `httprequest:"GET /v1/u/:username/ssh-keys"` Username Username `httprequest:"username,path"` } // UserSSHKeysResponse holds a response to the GET /v1/u/:username/ssh-keys // containing list of ssh keys associated with the user. type SSHKeysResponse struct { SSHKeys []string `json:"ssh_keys"` } // PutSSHKeysRequest is a request to set ssh keys to the list of ssh keys // associated with the user. type PutSSHKeysRequest struct { httprequest.Route `httprequest:"PUT /v1/u/:username/ssh-keys"` Username Username `httprequest:"username,path"` Body PutSSHKeysBody `httprequest:",body"` } // PutSSHKeysBody holds the body of a PutSSHKeysRequest. type PutSSHKeysBody struct { SSHKeys []string `json:"ssh-keys"` Add bool `json:"add,omitempty"` } // DeleteSSHKeysRequest is a request to remove ssh keys from the list of ssh keys // associated with the user. type DeleteSSHKeysRequest struct { httprequest.Route `httprequest:"DELETE /v1/u/:username/ssh-keys"` Username Username `httprequest:"username,path"` Body DeleteSSHKeysBody `httprequest:",body"` } // DeleteSSHKeysBody holds the body of a DeleteSSHKeysRequest. type DeleteSSHKeysBody struct { SSHKeys []string `json:"ssh-keys"` } // UserExtraInfoRequest is a request for the arbitrary extra information // stored about the user. type UserExtraInfoRequest struct { httprequest.Route `httprequest:"GET /v1/u/:username/extra-info"` Username Username `httprequest:"username,path"` } // SetUserExtraInfoRequest is a request to updated the arbitrary extra // information stored about the user. type SetUserExtraInfoRequest struct { httprequest.Route `httprequest:"PUT /v1/u/:username/extra-info"` Username Username `httprequest:"username,path"` ExtraInfo map[string]interface{} `httprequest:",body"` } // UserExtraInfoItemRequest is a request for a single element of the // arbitrary extra information stored about the user. type UserExtraInfoItemRequest struct { httprequest.Route `httprequest:"GET /v1/u/:username/extra-info/:item"` Username Username `httprequest:"username,path"` Item string `httprequest:"item,path"` } // SetUserExtraInfoItemRequest is a request to update a single element of // the arbitrary extra information stored about the user. type SetUserExtraInfoItemRequest struct { httprequest.Route `httprequest:"PUT /v1/u/:username/extra-info/:item"` Username Username `httprequest:"username,path"` Item string `httprequest:"item,path"` Data interface{} `httprequest:",body"` } // WhoAmIRequest holds parameters for requesting the current user name. type WhoAmIRequest struct { httprequest.Route `httprequest:"GET /v1/whoami"` } // WhoAmIResponse holds information on the currently // authenticated user. type WhoAmIResponse struct { User string `json:"user"` } // DischargeTokenForUserRequest is the request to get a discharge token // for a specific user. type DischargeTokenForUserRequest struct { httprequest.Route `httprequest:"GET /v1/discharge-token-for-user"` Username Username `httprequest:"username,form"` } // DischargeTokenForUserResponse holds the discharge token, in the form // of a macaroon, for the requested user. type DischargeTokenForUserResponse struct { DischargeToken *bakery.Macaroon } // IDPChoice lists available IDPs for authentication. type IDPChoice struct { IDPs []IDPChoiceDetails `json:"idps"` } // IDPChoiceDetails provides details about a IDP choice for authentication. type IDPChoiceDetails struct { Domain string `json:"domain"` Description string `json:"description"` Icon string `json:"icon"` Name string `json:"name"` URL string `json:"url"` } // GetUserWithIDRequest is a request for the user details of the user with the // given ID. type GetUserWithIDRequest struct { httprequest.Route `httprequest:"GET /v1/uid"` UserID string `httprequest:"id,form"` } // GetUserGroupsWithIDRequest is a request for the groups of the user with the // given ID. type GetUserGroupsWithIDRequest struct { httprequest.Route `httprequest:"GET /v1/uid/groups"` UserID string `httprequest:"id,form"` } // GroupsResponse is the response to a GetUserGroupsWithIDRequest. type GroupsResponse struct { Groups []string `json:"groups"` } // ClearUserMFACredentialsRequest is a request to delete // all MFA credentials for a user. type ClearUserMFACredentialsRequest struct { httprequest.Route `httprequest:"DELETE /v1/mfa/:username"` Username Username `httprequest:"username,path"` } var ( // BrandName holds the brand name of the entity running Candid. BrandName string // BrandLogoLocation holds the logo location of the entity running // Candid. BrandLogoLocation string ) // TemplateBrandParameters holds branding information for the entity // running Candid. type TemplateBrandParameters struct { // BrandName holds the brand name of the entity running Candid. BrandName string // LogoLocation holds the logo location of the entity running // Candid. BrandLogoLocation string } // BrandParameters returns branding information for the entity // running Candid. func BrandParameters() TemplateBrandParameters { return TemplateBrandParameters{ BrandName: BrandName, BrandLogoLocation: BrandLogoLocation, } } golang-github-canonical-candid-1.12.3/params/params_test.go000066400000000000000000000023011457263123000236250ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE.client file for details. package params_test import ( "testing" qt "github.com/frankban/quicktest" "github.com/canonical/candid/params" ) var usernameUnmarshalTests = []struct { username string expectError string }{{ username: "user", }, { username: "admin@idm", }, { username: "agent@admin@idm", }, { username: "toolongusername_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef_", expectError: "username longer than 256 characters", }} func TestUsernameTextUnmarshal(t *testing.T) { c := qt.New(t) defer c.Done() for _, test := range usernameUnmarshalTests { c.Run(test.username, func(c *qt.C) { u := new(params.Username) err := u.UnmarshalText([]byte(test.username)) if test.expectError == "" { c.Assert(err, qt.IsNil) c.Assert(*u, qt.Equals, params.Username(test.username)) } else { c.Assert(err, qt.ErrorMatches, test.expectError) c.Assert(*u, qt.Equals, params.Username("")) } }) } } golang-github-canonical-candid-1.12.3/scripts/000077500000000000000000000000001457263123000211645ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/scripts/docker-github-auth.sh000077500000000000000000000011601457263123000252070ustar00rootroot00000000000000#!/bin/bash -e if [[ ! -z "${GH_SSH_KEY}" ]]; then echo 'Using SSH auth ($GH_SSH_KEY)' mkdir $HOME/.ssh/ echo "$GH_SSH_KEY" > $HOME/.ssh/id_rsa chmod 600 $HOME/.ssh/id_rsa touch $HOME/.ssh/known_hosts ssh-keyscan github.com >> $HOME/.ssh/known_hosts git config --global --add url."git@github.com:".insteadOf "https://github.com/" elif [[ ! -z "${GH_USERNAME}" ]]; then echo 'Using basic auth ($GH_USERNAME and $GH_PASSWORD)' echo "machine github.com login $GH_USERNAME password $GH_PASSWORD" > $HOME/.netrc chmod 600 $HOME/.netrc else echo 'No Github SSH or basic auth credentials. Doing nothing.' fi golang-github-canonical-candid-1.12.3/scripts/image-build.sh000077500000000000000000000015111457263123000237000ustar00rootroot00000000000000 #!/bin/bash # This script builds the candid docker image and pushes it to the # configured docker registry, # Required environment variables: # - DOCKER_REGISTRY # - GIT_VERSION (optional) # - http_proxy # - https_proxy # - no_proxy set -ex # If there is a GIT_VERSION set, use that instead of master if [ -n "${GIT_VERSION}" ]; then git checkout $GIT_VERSION fi VERSION=`git describe --dirty --always` GIT_COMMIT=`git rev-parse --verify HEAD` docker build \ --build-arg http_proxy \ --build-arg https_proxy \ --build-arg no_proxy \ --build-arg NO_PROXY \ -t ${DOCKER_REGISTRY}/candid:${VERSION} \ -f ./Dockerfile . docker tag ${DOCKER_REGISTRY}/candid:${VERSION} ${DOCKER_REGISTRY}/candid:latest docker push ${DOCKER_REGISTRY}/candid:${VERSION} docker push ${DOCKER_REGISTRY}/candid:latest golang-github-canonical-candid-1.12.3/scripts/lxd-charm-build.sh000077500000000000000000000033541457263123000245040ustar00rootroot00000000000000#!/bin/sh # lxd-snap-build.sh - build Candid charm in a clean LXD environment set -eu charm_name=candid image=${image:-ubuntu:20.04} container=${container:-${charm_name}-charm-`uuidgen`} lxd_exec() { lxc exec \ --env http_proxy=${http_proxy:-} \ --env https_proxy=${https_proxy:-${http_proxy:-}} \ --env no_proxy=${no_proxy:-} \ $container -- "$@" } lxd_exec_ubuntu() { lxc exec \ --env HOME=/home/ubuntu \ --env http_proxy=${http_proxy:-} \ --env https_proxy=${https_proxy:-${http_proxy:-}} \ --env no_proxy=${no_proxy:-} \ --user 1000 \ --group 1000 \ --cwd=${cwd:-/home/ubuntu} \ $container -- "$@" } lxc launch -e ${image} $container trap "lxc stop $container" EXIT lxd_exec sh -c 'while [ ! -f /var/lib/cloud/instance/boot-finished ]; do sleep 0.1; done' lxd_exec apt-get update -q -y lxd_exec apt-get upgrade -q -y lxd_exec apt-get install -y build-essential autoconf python-dev-is-python3 if [ -n "${http_proxy:-}" ]; then lxd_exec snap set system proxy.http=${http_proxy:-} lxd_exec snap set system proxy.https=${https_proxy:-${http_proxy:-}} lxd_exec_ubuntu git config --global http.proxy ${http_proxy:-} fi lxd_exec snap install charmcraft --classic echo "Push .netrc" lxc file push --uid 1000 --gid 1000 --mode 600 ${NETRC:-$HOME/.netrc} $container/home/ubuntu/.netrc echo "Create src" lxd_exec_ubuntu mkdir -p /home/ubuntu/src echo "Transfer data" tar c -C `dirname $0`/.. . | cwd=/home/ubuntu/src lxd_exec_ubuntu tar x echo "Charmcraft build" cwd=/home/ubuntu/src/charms/candid lxd_exec_ubuntu sudo -E charmcraft pack --verbose --destructive-mode echo "Find file" charmfile=`lxd_exec_ubuntu find /home/ubuntu/src/charms/candid -name "${charm_name}_*.charm"| head -1` echo "Pull file" lxc file pull $container$charmfile . golang-github-canonical-candid-1.12.3/scripts/lxd-charm-k8s-build.sh000077500000000000000000000033701457263123000252050ustar00rootroot00000000000000#!/bin/sh # lxd-snap-build.sh - build Candid charm in a clean LXD environment set -eu charm_name=candid-k8s image=${image:-ubuntu:20.04} container=${container:-${charm_name}-charm-`uuidgen`} lxd_exec() { lxc exec \ --env http_proxy=${http_proxy:-} \ --env https_proxy=${https_proxy:-${http_proxy:-}} \ --env no_proxy=${no_proxy:-} \ $container -- "$@" } lxd_exec_ubuntu() { lxc exec \ --env HOME=/home/ubuntu \ --env http_proxy=${http_proxy:-} \ --env https_proxy=${https_proxy:-${http_proxy:-}} \ --env no_proxy=${no_proxy:-} \ --user 1000 \ --group 1000 \ --cwd=${cwd:-/home/ubuntu} \ $container -- "$@" } lxc launch -e ${image} $container trap "lxc stop $container" EXIT lxd_exec sh -c 'while [ ! -f /var/lib/cloud/instance/boot-finished ]; do sleep 0.1; done' lxd_exec apt-get update -q -y lxd_exec apt-get upgrade -q -y lxd_exec apt-get install -y build-essential autoconf python-dev-is-python3 if [ -n "${http_proxy:-}" ]; then lxd_exec snap set system proxy.http=${http_proxy:-} lxd_exec snap set system proxy.https=${https_proxy:-${http_proxy:-}} lxd_exec_ubuntu git config --global http.proxy ${http_proxy:-} fi lxd_exec snap install charmcraft --classic echo "Push .netrc" lxc file push --uid 1000 --gid 1000 --mode 600 ${NETRC:-$HOME/.netrc} $container/home/ubuntu/.netrc echo "Create src" lxd_exec_ubuntu mkdir -p /home/ubuntu/src echo "Transfer data" tar c -C `dirname $0`/.. . | cwd=/home/ubuntu/src lxd_exec_ubuntu tar x echo "Charmcraft build" cwd=/home/ubuntu/src/charms/candid-k8s lxd_exec_ubuntu sudo -E charmcraft pack --verbose --destructive-mode echo "Find file" charmfile=`lxd_exec_ubuntu find /home/ubuntu/src/charms/candid-k8s -name "${charm_name}_*.charm"| head -1` echo "Pull file" lxc file pull $container$charmfile . golang-github-canonical-candid-1.12.3/scripts/lxd-snap-build.sh000077500000000000000000000031501457263123000243450ustar00rootroot00000000000000#!/bin/sh # lxd-snap-build.sh - build Candid snap in a clean LXD environment set -eu snap_name=${snap_name:-candid} image=${image:-ubuntu:20.04} container=${container:-${snap_name}-snap-`uuidgen`} lxd_exec() { lxc exec \ --env http_proxy=${http_proxy:-} \ --env https_proxy=${https_proxy:-${http_proxy:-}} \ --env no_proxy=${no_proxy:-} \ $container -- "$@" } lxd_exec_ubuntu() { lxc exec \ --env HOME=/home/ubuntu \ --env http_proxy=${http_proxy:-} \ --env https_proxy=${https_proxy:-${http_proxy:-}} \ --env no_proxy=${no_proxy:-} \ --user 1000 \ --group 1000 \ --cwd=${cwd:-/home/ubuntu} \ $container -- "$@" } lxc launch -e ${image} $container trap "lxc stop $container" EXIT lxd_exec sh -c 'while [ ! -f /var/lib/cloud/instance/boot-finished ]; do sleep 0.1; done' lxd_exec apt-get update -q -y lxd_exec apt-get upgrade -q -y if [ -n "${http_proxy:-}" ]; then lxd_exec snap set system proxy.http=${http_proxy:-} lxd_exec snap set system proxy.https=${https_proxy:-${http_proxy:-}} lxd_exec_ubuntu git config --global http.proxy ${http_proxy:-} fi lxd_exec snap install snapcraft --classic lxc file push --uid 1000 --gid 1000 --mode 600 ${NETRC:-$HOME/.netrc} $container/home/ubuntu/.netrc lxd_exec_ubuntu mkdir -p /home/ubuntu/src tar c -C `dirname $0`/.. . | cwd=/home/ubuntu/src lxd_exec_ubuntu tar x target= if [ -n "${target_arch:-}" ]; then target="--target-arch ${target_arch}" fi cwd=/home/ubuntu/src lxd_exec_ubuntu snapcraft --destructive-mode $target snapfile=`lxd_exec_ubuntu find /home/ubuntu/src -name "${snap_name}_*.snap"| head -1` lxc file pull $container$snapfile . echo $snapfile golang-github-canonical-candid-1.12.3/scripts/set-version.sh000077500000000000000000000004551457263123000240050ustar00rootroot00000000000000#!/bin/sh # # set-version.sh # Set the version built into the application. set -e if [ -z "${GIT_COMMIT}" ]; then exit 0 fi if [ -z "${VERSION}" ]; then exit 0 fi gofmt -r "unknownVersion -> Version{GitCommit: \"${GIT_COMMIT}\", Version: \"${VERSION}\",}" version/init.go.tmpl > version/init.go golang-github-canonical-candid-1.12.3/server.go000066400000000000000000000125471457263123000213430ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package candid import ( "html/template" "net/http" "sort" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/aclstore/v2" "github.com/juju/utils/v2/debugstatus" "gopkg.in/errgo.v1" "github.com/canonical/candid/idp" "github.com/canonical/candid/idp/agent" "github.com/canonical/candid/internal/debug" "github.com/canonical/candid/internal/discharger" "github.com/canonical/candid/internal/identity" "github.com/canonical/candid/internal/mfa" v1 "github.com/canonical/candid/internal/v1" "github.com/canonical/candid/meeting" "github.com/canonical/candid/store" ) // Versions of the API that can be served. const ( Debug = "debug" Discharger = "discharger" V1 = "v1" ) var versions = map[string]identity.NewAPIHandlerFunc{ Debug: debug.NewAPIHandler, Discharger: discharger.NewAPIHandler, V1: v1.NewAPIHandler, } // Versions returns all known API version strings in alphabetical order. func Versions() []string { vs := make([]string, 0, len(versions)) for v := range versions { vs = append(vs, v) } sort.Strings(vs) return vs } // ServerParams contains configuration parameters for a server. type ServerParams struct { // MeetingStore holds the storage that will be used to store // rendezvous information. MeetingStore meeting.Store // ProviderDataStore holds the storeage that can be used by // identity providers to store data that is not associated with // an individual identity. ProviderDataStore store.ProviderDataStore // RootKeyStore holds the root key store that will be used to // store macaroon root keys within the identity server. RootKeyStore bakery.RootKeyStore // Store holds the identities store for the identity server. Store store.Store // AdminPassword holds the password for admin login. AdminPassword string // Key holds the keypair to use with the bakery service. Key *bakery.KeyPair // Location holds a URL representing the externally accessible // base URL of the service, without a trailing slash. Location string // PrivateAddr should hold a dialable address that will be used // for communication between identity servers. Note that this // should not contain a port. PrivateAddr string // IdentityProviders contains the set of identity providers that // should be initialised by the service. IdentityProviders []idp.IdentityProvider // MFAAuthenticator holds the multi-factor authenticator. MFAAuthenticator *mfa.Authenticator // DebugTeams contains the set of launchpad teams that may access // the restricted debug endpoints. // TODO remove this. DebugTeams []string // AdminAgentPublicKey contains the public key of the admin agent. AdminAgentPublicKey *bakery.PublicKey // StaticFileSystem contains an http.FileSystem that can be used // to serve static files. StaticFileSystem http.FileSystem // Template contains a set of templates that are used to generate // html output. Template *template.Template // DebugStatusCheckerFuncs contains functions that will be // executed as part of a /debug/status check. DebugStatusCheckerFuncs []debugstatus.CheckerFunc // RendezvousTimeout holds the time after which an interactive discharge wait // request will time out. RendezvousTimeout time.Duration // ACLStore holds the ACLStore for the identity server. ACLStore aclstore.ACLStore // RedirectLoginTrustedURLs contains a list of URLs that are // trusted to be used as return_to URLs during an interactive // login. RedirectLoginTrustedURLs []string // RedirectLoginTrustedDomains contains a list of domain names that // are fully trusted to be used as return_to URLs during an // interactive login. If the domain starts with the sequence "*." // then all subdomains of the subsequent domain will be trusted. RedirectLoginTrustedDomains []string // APIMacaroonTimeout is the maximum life of an API macaroon. APIMacaroonTimeout time.Duration // DischargeMacaroonTimeout is the maximum life of a Discharge // macaroon. DischargeMacaroonTimeout time.Duration // DischargeTokenTimeout is the maximum life of a Discharge // token. DischargeTokenTimeout time.Duration // SkipLocationForCookiePaths instructs if the Cookie Paths are to // be set relative to the Location Path or not. SkipLocationForCookiePaths bool // EnableEmailLogin enables the login with email address link on the // authentication required page. EnableEmailLogin bool } // NewServer returns a new handler that handles identity service requests and // stores its data in the given database. The handler will serve the specified // versions of the API. func NewServer(params ServerParams, serveVersions ...string) (HandlerCloser, error) { // Remove the agent identity provider if it is specified as it is no longer used. idps := make([]idp.IdentityProvider, 0, len(params.IdentityProviders)) for _, idp := range params.IdentityProviders { if idp == agent.IdentityProvider { continue } idps = append(idps, idp) } params.IdentityProviders = idps newAPIs := make(map[string]identity.NewAPIHandlerFunc) for _, vers := range serveVersions { newAPI := versions[vers] if newAPI == nil { return nil, errgo.Newf("unknown version %q", vers) } newAPIs[vers] = newAPI } return identity.New(identity.ServerParams(params), newAPIs) } type HandlerCloser interface { http.Handler Close() } golang-github-canonical-candid-1.12.3/server_test.go000066400000000000000000000055521457263123000224000ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package candid_test import ( "net/http" "testing" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/juju/qthttptest" yaml "gopkg.in/yaml.v2" "github.com/canonical/candid" "github.com/canonical/candid/config" "github.com/canonical/candid/idp" _ "github.com/canonical/candid/idp/agent" _ "github.com/canonical/candid/idp/static" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/version" ) func TestServer(t *testing.T) { qtsuite.Run(qt.New(t), &serverSuite{}) } type serverSuite struct { store *candidtest.Store } func (s *serverSuite) Init(c *qt.C) { s.store = candidtest.NewStore() } func (s *serverSuite) TestNewServerWithNoVersions(c *qt.C) { h, err := candid.NewServer(candid.ServerParams(s.store.ServerParams())) c.Assert(err, qt.ErrorMatches, `identity server must serve at least one version of the API`) c.Assert(h, qt.IsNil) } func (s *serverSuite) TestNewServerWithUnregisteredVersion(c *qt.C) { h, err := candid.NewServer( candid.ServerParams(s.store.ServerParams()), "wrong", ) c.Assert(err, qt.ErrorMatches, `unknown version "wrong"`) c.Assert(h, qt.IsNil) } type versionResponse struct { Version string Path string } func (s *serverSuite) TestVersions(c *qt.C) { c.Assert(candid.Versions(), qt.DeepEquals, []string{"debug", "discharger", "v1"}) } func (s *serverSuite) TestNewServerWithVersions(c *qt.C) { h, err := candid.NewServer( candid.ServerParams(s.store.ServerParams()), candid.Debug, ) c.Assert(err, qt.IsNil) defer h.Close() qthttptest.AssertJSONCall(c, qthttptest.JSONCallParams{ Handler: h, URL: "/debug/info", ExpectStatus: http.StatusOK, ExpectBody: version.VersionInfo, }) assertDoesNotServeVersion(c, h, "v0") } func (s *serverSuite) TestNewServerRemovesAgentIDP(c *qt.C) { var conf config.Config err := yaml.Unmarshal([]byte(`{"identity-providers": [{"type":"agent"},{"type":"static","name":"test"}]}`), &conf) c.Assert(err, qt.IsNil) idps := make([]idp.IdentityProvider, len(conf.IdentityProviders)) for i, idp := range conf.IdentityProviders { idps[i] = idp.IdentityProvider } sp := candid.ServerParams(s.store.ServerParams()) sp.IdentityProviders = idps h, err := candid.NewServer(sp, candid.V1) c.Assert(err, qt.IsNil) h.Close() } func assertServesVersion(c *qt.C, h http.Handler, vers string) { qthttptest.AssertJSONCall(c, qthttptest.JSONCallParams{ Handler: h, URL: "/" + vers + "/some/path", ExpectBody: versionResponse{ Version: vers, Path: "/some/path", }, }) } func assertDoesNotServeVersion(c *qt.C, h http.Handler, vers string) { rec := qthttptest.DoRequest(c, qthttptest.DoRequestParams{ Handler: h, URL: "/" + vers + "/some/path", }) c.Assert(rec.Code, qt.Equals, http.StatusNotFound) } golang-github-canonical-candid-1.12.3/snap/000077500000000000000000000000001457263123000204365ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/snap/local/000077500000000000000000000000001457263123000215305ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/snap/local/config/000077500000000000000000000000001457263123000227755ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/snap/local/config/config.yaml000066400000000000000000000023471457263123000251340ustar00rootroot00000000000000## Documentation can be found here: https://github.com/canonical/candid/blob/master/docs/configuration.md ## Server URLs and ports listen-address: :8081 private-addr: 127.0.0.1 location: 'http://%LOCATION%:8081' ## Persistent storage # Defaults to non-persistent memory storage, install PostgreSQL or MongoDB # and configure them below before using this service in production storage: type: memory #storage: # type: mongodb # address: 127.0.0.1:27017 #storage: # type: postgres # connection-string: postgres://user:pass@localhost/candid ## Identity providers # Configure this with whatever authentication system you're using identity-providers: - type: static name: static users: user1: name: User One email: user1@example.com password: password1 groups: - group1 - group3 user2: name: User Two email: user2@example.com password: password2 groups: - group2 - group3 ## Logging logging-config: INFO ## Authentication keys public-key: %PUBLIC-KEY% private-key: %PRIVATE-KEY% admin-agent-public-key: %ADMIN-PUBLIC-KEY% # Don't change, snap-specific paths access-log: /var/snap/candid/common/logs/candid.access.log resource-path: /snap/candid/current/www/ golang-github-canonical-candid-1.12.3/snap/local/wrappers/000077500000000000000000000000001457263123000233735ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/snap/local/wrappers/candid000077500000000000000000000003341457263123000245430ustar00rootroot00000000000000#!/bin/sh -eu url=$(grep ^location /var/snap/candid/current/config.yaml | cut -d: -f2- | sed "s/[ '\"]//g" || true) if [ -z "${CANDID_URL:-}" ] && [ -n "${url}" ]; then export CANDID_URL=${url} fi exec candid "$@" golang-github-canonical-candid-1.12.3/snap/local/wrappers/candidsrv000077500000000000000000000021351457263123000252770ustar00rootroot00000000000000#!/bin/bash -eu CONFIG_FILE="${SNAP_DATA}/config.yaml" ADMIN_AGENT_FILE="${SNAP_DATA}/admin.keys" if [ ! -e "$CONFIG_FILE" ]; then cp "${SNAP}/config/config.yaml" "$CONFIG_FILE" # replace hostname hostname="$(hostname -f)" if [ -n "$hostname" ]; then sed -i "s#%LOCATION%#${hostname}#g" "$CONFIG_FILE" fi # setup keys key="$(bakery-keygen)" private_key=$(echo "$key" | jq -r .private) public_key=$(echo "$key" | jq -r .public) sed -i "s#%PRIVATE-KEY%#${private_key}#g" "$CONFIG_FILE" sed -i "s#%PUBLIC-KEY%#${public_key}#g" "$CONFIG_FILE" # create admin credentials admin_key="$(bakery-keygen)" admin_private_key=$(echo "$admin_key" | jq -r .private) admin_public_key=$(echo "$admin_key" | jq -r .public) cat >"$ADMIN_AGENT_FILE" < Version{GitCommit: \"${GIT_COMMIT}\", Version: \"${GIT_VERSION}\",}" version/init.go GOBIN=${SNAPCRAFT_PART_INSTALL}/bin/ go install github.com/canonical/candid/cmd/candid GOBIN=${SNAPCRAFT_PART_INSTALL}/bin/ go install github.com/canonical/candid/cmd/candidsrv GOBIN=${SNAPCRAFT_PART_INSTALL}/bin/ go install gopkg.in/macaroon-bakery.v2/cmd/bakery-keygen@latest www-static: plugin: dump source: ./static source-type: local override-build: | mkdir -p $SNAPCRAFT_PART_INSTALL/www/static mv ./* $SNAPCRAFT_PART_INSTALL/www/static www-templates: plugin: dump source: ./templates source-type: local override-build: | mkdir -p $SNAPCRAFT_PART_INSTALL/www/templates mv ./* $SNAPCRAFT_PART_INSTALL/www/templates local: plugin: dump source: snap/local/ prime: - config - wrappers golang-github-canonical-candid-1.12.3/static/000077500000000000000000000000001457263123000207645ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/static/css/000077500000000000000000000000001457263123000215545ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/static/css/login-form.css000066400000000000000000000001051457263123000243330ustar00rootroot00000000000000.back-to-login .p-icon--chevron-up { transform: rotate(90deg); } golang-github-canonical-candid-1.12.3/static/css/mfa-manage.css000066400000000000000000000014171457263123000242620ustar00rootroot00000000000000.security-keys__item { display: flex; justify-content: space-between; align-items: center; padding: .25rem .5rem; transition: all linear .2s; } .security-keys__item--removable { cursor: pointer; } .security-keys__item--removable:hover { background-color: rgba(0, 0, 0, 0.05); } .security-keys__item--remove-confirm { background-color: rgba(0, 0, 0, 0.05); margin-bottom: 1rem; } .remove-security-key-button { border: none; background: transparent; opacity: 0; visibility: hidden; transition: all linear .2s; } .security-keys__item--removable:hover .remove-security-key-button { opacity: 1; visibility: visible; } .confirmation-row { display: flex; justify-content: flex-end; align-items: center; } golang-github-canonical-candid-1.12.3/static/css/vanilla-framework-version-2.24.1.min.css000066400000000000000000007424761457263123000307220ustar00rootroot00000000000000/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}h1{font-size:2em;margin:0.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace, monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%}button,input{overflow:visible}button,select{text-transform:none}button,[type='button'],[type='reset'],[type='submit']{-webkit-appearance:button}button::-moz-focus-inner,[type='button']::-moz-focus-inner,[type='reset']::-moz-focus-inner,[type='submit']::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type='button']:-moz-focusring,[type='reset']:-moz-focusring,[type='submit']:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type='checkbox'],[type='radio']{box-sizing:border-box;padding:0}[type='number']::-webkit-inner-spin-button,[type='number']::-webkit-outer-spin-button{height:auto}[type='search']{-webkit-appearance:textfield;outline-offset:-2px}[type='search']::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}h1,[type='checkbox']+label.is-h1,[type='radio']+label.is-h1,.p-heading--1,.p-heading--one,.p-media-object--large .p-media-object__title{max-width:40em;font-style:normal;font-weight:100;margin-top:0}@media (max-width: 772px){h1,[type='checkbox']+label.is-h1,[type='radio']+label.is-h1,.p-heading--1,.p-heading--one,.p-media-object--large .p-media-object__title{font-size:2.22819rem;line-height:3rem;margin-bottom:.835rem;padding-top:0.166rem}}@media (min-width: 772px){h1,[type='checkbox']+label.is-h1,[type='radio']+label.is-h1,.p-heading--1,.p-heading--one,.p-media-object--large .p-media-object__title{font-size:2.91029rem;line-height:3.5rem;margin-bottom:.8rem;padding-top:0.201rem}}@media (min-width: 1681px){h1,[type='checkbox']+label.is-h1,[type='radio']+label.is-h1,.p-heading--1,.p-heading--one,.p-media-object--large .p-media-object__title{margin-bottom:.85rem;padding-top:0.151rem}}h2,[type='checkbox']+label.is-h2,[type='radio']+label.is-h2,.p-heading--2,.p-heading--two{max-width:40em;font-style:normal;font-weight:100;margin-top:0}@media (max-width: 772px){h2,[type='checkbox']+label.is-h2,[type='radio']+label.is-h2,.p-heading--2,.p-heading--two{font-size:1.83274rem;line-height:2.5rem;margin-bottom:.9rem;padding-top:0.101rem}}@media (min-width: 772px){h2,[type='checkbox']+label.is-h2,[type='radio']+label.is-h2,.p-heading--2,.p-heading--two{font-size:2.22819rem;line-height:3rem;margin-bottom:.8rem;padding-top:0.201rem}}h3,[type='checkbox']+label.is-h3,[type='radio']+label.is-h3,.p-heading--3,.p-heading--three,.p-pull-quote--large .p-pull-quote__quote{max-width:40em;font-style:normal;font-weight:100;margin-top:0}@media (max-width: 772px){h3,[type='checkbox']+label.is-h3,[type='radio']+label.is-h3,.p-heading--3,.p-heading--three,.p-pull-quote--large .p-pull-quote__quote{font-size:1.49271rem;line-height:2rem;margin-bottom:.5rem;padding-top:0.5rem}}@media (min-width: 772px){h3,[type='checkbox']+label.is-h3,[type='radio']+label.is-h3,.p-heading--3,.p-heading--three,.p-pull-quote--large .p-pull-quote__quote{font-size:1.70596rem;line-height:2.5rem;margin-bottom:.9rem;padding-top:0.101rem}}h4,[type='checkbox']+label.is-h4,[type='radio']+label.is-h4,.p-heading--4,.p-heading--four,.p-matrix__title,.p-media-object__title,.p-modal__title,.p-pull-quote .p-pull-quote__quote,.p-panel__title{max-width:40em;font-style:normal;font-weight:300;margin-top:0}@media (max-width: 772px){h4,[type='checkbox']+label.is-h4,[type='radio']+label.is-h4,.p-heading--4,.p-heading--four,.p-matrix__title,.p-media-object__title,.p-modal__title,.p-pull-quote .p-pull-quote__quote,.p-panel__title{font-size:1.22176rem;line-height:1.5rem;margin-bottom:.7rem;padding-top:0.301rem}}@media (min-width: 772px){h4,[type='checkbox']+label.is-h4,[type='radio']+label.is-h4,.p-heading--4,.p-heading--four,.p-matrix__title,.p-media-object__title,.p-modal__title,.p-pull-quote .p-pull-quote__quote,.p-panel__title{font-size:1.30612rem;line-height:2rem;margin-bottom:.95rem;padding-top:0.051rem}}@media (min-width: 1681px){h4,[type='checkbox']+label.is-h4,[type='radio']+label.is-h4,.p-heading--4,.p-heading--four,.p-matrix__title,.p-media-object__title,.p-modal__title,.p-pull-quote .p-pull-quote__quote,.p-panel__title{margin-bottom:1rem;padding-top:.001rem}}h5,.p-code-snippet .p-code-snippet__title,.p-heading--5,.p-heading--five{max-width:40em;font-size:1rem;font-style:normal;font-weight:500;line-height:1.5rem;margin-bottom:1.1rem;margin-top:0;padding-top:.401rem}h6,.p-heading--6,.p-heading--six,.p-pull-quote .p-pull-quote__citation,.p-pull-quote--small .p-pull-quote__citation,.p-pull-quote--large .p-pull-quote__citation{max-width:40em;font-size:1rem;font-style:italic;font-weight:300;line-height:1.5rem;margin-bottom:1.1rem;margin-top:0;padding-top:0.338rem}@media (min-width: 1681px){h6,.p-heading--6,.p-heading--six,.p-pull-quote .p-pull-quote__citation,.p-pull-quote--small .p-pull-quote__citation,.p-pull-quote--large .p-pull-quote__citation{padding-top:0.345rem}}.p-text--default,cite,dd,dt,.p-breadcrumbs__item,.p-pull-quote--small .p-pull-quote__quote,p,summary,.p-side-navigation--raw-html h2,.p-side-navigation--raw-html h3,.p-side-navigation--raw-html h4,.p-side-navigation--raw-html h5,.p-side-navigation--raw-html h6{line-height:1.5rem;margin-top:0;padding-top:.4005rem}.p-text--default,cite,dd,dt,.p-breadcrumbs__item,.p-pull-quote--small .p-pull-quote__quote{margin-bottom:.1rem}p{margin-bottom:1.1rem}p:not([class*='p-heading--']):not([class*='p-muted-heading'])+p{margin-top:-.5rem}[type='checkbox']+label.is-muted-heading,[type='radio']+label.is-muted-heading,[type='checkbox']+label.is-muted-inline-heading,[type='radio']+label.is-muted-inline-heading,.p-muted-heading,.p-table--mobile-card td::before,.p-table--mobile-card tbody th::before,small,.p-text--small,.p-chip,.p-chip__value,.p-form-help-text,.p-form-validation__message,.p-media-object__meta-list-item--date,.p-media-object__meta-list-item--location,.p-media-object__meta-list-item--venue,.p-media-object__meta-list-item,.p-tooltip__message{font-size:.875rem;line-height:1rem;margin-bottom:.8rem;padding-top:0.2005rem}@media (min-width: 1681px){[type='checkbox']+label.is-muted-heading,[type='radio']+label.is-muted-heading,[type='checkbox']+label.is-muted-inline-heading,[type='radio']+label.is-muted-inline-heading,.p-muted-heading,.p-table--mobile-card td::before,.p-table--mobile-card tbody th::before,small,.p-text--small,.p-chip,.p-chip__value,.p-form-help-text,.p-form-validation__message,.p-media-object__meta-list-item--date,.p-media-object__meta-list-item--location,.p-media-object__meta-list-item--venue,.p-media-object__meta-list-item,.p-tooltip__message{padding-top:0.2006rem}}[type='checkbox']+label.is-table-header,[type='radio']+label.is-table-header,thead th,.p-text--x-small,.p-text--x-small-capitalized,.p-chip__lead,.p-label--validated,.p-label--in-progress,.p-label--new,.p-label--updated,.p-label--deprecated{font-size:.76562rem;line-height:1rem;margin-bottom:.75rem;padding-top:0.2505rem}@media (min-width: 1681px){[type='checkbox']+label.is-table-header,[type='radio']+label.is-table-header,thead th,.p-text--x-small,.p-text--x-small-capitalized,.p-chip__lead,.p-label--validated,.p-label--in-progress,.p-label--new,.p-label--updated,.p-label--deprecated{padding-top:0.2506rem}}[type='checkbox']+label.is-muted-heading,[type='radio']+label.is-muted-heading,[type='checkbox']+label.is-muted-inline-heading,[type='radio']+label.is-muted-inline-heading,.p-muted-heading,.p-table--mobile-card td::before,.p-table--mobile-card tbody th::before,.u-text--muted{color:#666}[type='checkbox']+label.is-table-header,[type='radio']+label.is-table-header,thead th{font-weight:400}[type='checkbox']+label.is-muted-heading,[type='radio']+label.is-muted-heading,[type='checkbox']+label.is-muted-inline-heading,[type='radio']+label.is-muted-inline-heading,.p-muted-heading,.p-table--mobile-card td::before,.p-table--mobile-card tbody th::before{font-weight:300;margin-bottom:.8rem;margin-top:0;padding-top:0.2rem;text-transform:uppercase}b,strong,dt,.p-notification__status,.p-side-navigation--raw-html h2,.p-side-navigation--raw-html h3,.p-side-navigation--raw-html h4,.p-side-navigation--raw-html h5,.p-side-navigation--raw-html h6{font-weight:400}p:not([class*='p-heading--']):not([class*='p-muted-heading'])+h1,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--1,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--one{padding-top:1.7rem}@media (max-width: 772px){p:not([class*='p-heading--']):not([class*='p-muted-heading'])+h1,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--1,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--one{padding-top:1.665rem}}p:not([class*='p-heading--']):not([class*='p-muted-heading'])+h2,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--2,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--two{padding-top:1.7rem}@media (max-width: 772px){p:not([class*='p-heading--']):not([class*='p-muted-heading'])+h2,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--2,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--two{padding-top:1.6rem}}@media (max-width: 772px){p:not([class*='p-heading--']):not([class*='p-muted-heading'])+h3,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--3,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--three{padding-top:2rem}}@media (min-width: 772px){p:not([class*='p-heading--']):not([class*='p-muted-heading'])+h3,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--3,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--three{padding-top:1.6rem}}@media (max-width: 772px){p:not([class*='p-heading--']):not([class*='p-muted-heading'])+h4,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--4,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--four{padding-top:1.8rem}}@media (min-width: 772px){p:not([class*='p-heading--']):not([class*='p-muted-heading'])+h4,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--4,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--four{padding-top:1.55rem}}p:not([class*='p-heading--']):not([class*='p-muted-heading'])+h5,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--5,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--five,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+h6,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--6,p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-heading--six{padding-top:1.9rem}p:not([class*='p-heading--']):not([class*='p-muted-heading'])+.p-muted-heading{padding-top:1.7rem}.p-card,.p-card--highlighted,.p-card--muted,.p-contextual-menu__dropdown,.p-modal__dialog,.p-subnav__items,.p-subnav__items--right,.p-notification,.p-notification--positive,.p-notification--caution,.p-notification--negative,.p-notification--information,.p-switch__slider,.p-switch__slider::before{border-radius:.125rem}.p-card--highlighted,.p-card--muted,.p-contextual-menu__dropdown,.p-modal__dialog,.p-subnav__items,.p-subnav__items--right,.p-notification,.p-notification--positive,.p-notification--caution,.p-notification--negative,.p-notification--information,.p-side-navigation:target .p-side-navigation__drawer,[class*='p-side-navigation--']:target .p-side-navigation__drawer,.p-side-navigation.is-expanded .p-side-navigation__drawer,[class*='p-side-navigation--'].is-expanded .p-side-navigation__drawer,.p-switch__slider::before{box-shadow:0 1px 5px 1px rgba(17,17,17,0.2)}.p-card{border:1px solid #cdcdcd}.p-card--muted{background-color:#f7f7f7;color:#111}.p-card,.p-card--highlighted,.p-modal__dialog,.p-notification,.p-notification--positive,.p-notification--caution,.p-notification--negative,.p-notification--information{background-color:#fff;color:#111}.p-card,.p-card--highlighted,.p-modal__dialog,.p-notification,.p-notification--positive,.p-notification--caution,.p-notification--negative,.p-notification--information{margin-bottom:1.5rem;overflow:auto;padding:1rem}td,th,.p-accordion__tab,.p-accordion__tab--with-title{padding-bottom:.5rem;padding-top:calc(.5rem - 1px)}.p-list,.p-list--divided,.is-ticked,.p-side-navigation__list,.p-side-navigation--raw-html ul{list-style:none;margin-left:0;padding-left:0}.p-modal__header::after,.p-side-navigation__list::after,.p-side-navigation--raw-html ul::after,.is-bordered[class*='p-strip']::after,.p-tabs__list::after,.p-accordion__group+.p-accordion__group::after,.p-tabs__link::before{background-color:#cdcdcd;content:'';height:1px;left:0;position:absolute;right:0}.p-modal__header,.p-side-navigation__list,.p-side-navigation--raw-html ul,.is-bordered[class*='p-strip'],.p-tabs__list{position:relative}.p-modal__header::after,.p-side-navigation__list::after,.p-side-navigation--raw-html ul::after,.is-bordered[class*='p-strip']::after,.p-tabs__list::after{bottom:0}.p-accordion__group+.p-accordion__group{position:relative}.p-accordion__group+.p-accordion__group::after{top:0}hr{border:0;height:1px;margin-top:0;position:relative;width:100%}@media only screen and (max-width: 1036px){.is-shallow[class*='p-strip'],.p-panel__content{padding-bottom:.75rem;padding-top:.75rem}}@media only screen and (min-width: 1036px){.is-shallow[class*='p-strip'],.p-panel__content{padding-bottom:1.5rem;padding-top:1.5rem}}@media only screen and (max-width: 1036px){.p-strip,.p-strip--light,.p-strip--dark,.p-strip--accent,.p-strip--image,.p-strip--suru,.p-strip--suru-topped{padding-bottom:2rem;padding-top:2rem}}@media only screen and (min-width: 1036px){.p-strip,.p-strip--light,.p-strip--dark,.p-strip--accent,.p-strip--image,.p-strip--suru,.p-strip--suru-topped{padding-bottom:4rem;padding-top:4rem}}@media only screen and (max-width: 1036px){.is-deep[class*='p-strip']{padding:3rem 0 3rem}}@media only screen and (min-width: 1036px){.is-deep[class*='p-strip']{padding:6rem 0}}.p-code-copyable::before,.p-code-snippet .p-code-snippet__block--icon::before,.p-icon--anchor,.p-icon--plus,.p-icon--minus,.p-icon--expand,.p-icon--collapse,.p-icon--chevron-down,.p-icon--chevron-up,.p-icon--contextual-menu,.p-icon--close,.p-icon--help,.p-icon--question,.p-icon--information,.p-icon--delete,.p-icon--error,.p-icon--warning,.p-icon--external-link,.p-icon--drag,.p-icon--code,.p-icon--menu,.p-icon--copy,.p-icon--search,.p-icon--success,.p-icon--share,.p-icon--user,.p-icon--spinner,.p-side-navigation__toggle::before,.p-side-navigation__toggle--in-drawer::before,.p-icon--facebook,.p-icon--twitter,.p-icon--instagram,.p-icon--linkedin,.p-icon--youtube,.p-icon--canonical,.p-icon--ubuntu,.p-icon--rss,.p-icon--email,.p-switch__slider span,.u-hide-text{overflow:hidden;text-indent:110vw;white-space:nowrap}.p-inline-images::after,.p-list::after,.p-stepped-list::after,.u-clearfix::after{clear:both;content:'';display:block}.u-no-margin--bottom:not(hr):not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):not(p):not(small):not([class*='p-heading']){margin-bottom:0 !important}@media (max-width: 772px){h1.u-no-margin--bottom,[type='checkbox']+label.u-no-margin--bottom.is-h1,[type='radio']+label.u-no-margin--bottom.is-h1,.u-no-margin--bottom.p-heading--1,.u-no-margin--bottom.p-heading--one,.p-media-object--large .u-no-margin--bottom.p-media-object__title{margin-bottom:-0.165rem !important}}@media (min-width: 772px){h1.u-no-margin--bottom,[type='checkbox']+label.u-no-margin--bottom.is-h1,[type='radio']+label.u-no-margin--bottom.is-h1,.u-no-margin--bottom.p-heading--1,.u-no-margin--bottom.p-heading--one,.p-media-object--large .u-no-margin--bottom.p-media-object__title{margin-bottom:-0.2rem !important}}@media (min-width: 1681px){h1.u-no-margin--bottom,[type='checkbox']+label.u-no-margin--bottom.is-h1,[type='radio']+label.u-no-margin--bottom.is-h1,.u-no-margin--bottom.p-heading--1,.u-no-margin--bottom.p-heading--one,.p-media-object--large .u-no-margin--bottom.p-media-object__title{margin-bottom:-0.15rem !important}}@media (max-width: 772px){h2.u-no-margin--bottom,[type='checkbox']+label.u-no-margin--bottom.is-h2,[type='radio']+label.u-no-margin--bottom.is-h2,.u-no-margin--bottom.p-heading--2,.u-no-margin--bottom.p-heading--two{margin-bottom:-0.1rem !important}}@media (min-width: 772px){h2.u-no-margin--bottom,[type='checkbox']+label.u-no-margin--bottom.is-h2,[type='radio']+label.u-no-margin--bottom.is-h2,.u-no-margin--bottom.p-heading--2,.u-no-margin--bottom.p-heading--two{margin-bottom:-0.2rem !important}}@media (max-width: 772px){h3.u-no-margin--bottom,[type='checkbox']+label.u-no-margin--bottom.is-h3,[type='radio']+label.u-no-margin--bottom.is-h3,.u-no-margin--bottom.p-heading--3,.u-no-margin--bottom.p-heading--three,.p-pull-quote--large .u-no-margin--bottom.p-pull-quote__quote{margin-bottom:0rem !important}}@media (min-width: 772px){h3.u-no-margin--bottom,[type='checkbox']+label.u-no-margin--bottom.is-h3,[type='radio']+label.u-no-margin--bottom.is-h3,.u-no-margin--bottom.p-heading--3,.u-no-margin--bottom.p-heading--three,.p-pull-quote--large .u-no-margin--bottom.p-pull-quote__quote{margin-bottom:-0.1rem !important}}@media (max-width: 772px){h4.u-no-margin--bottom,[type='checkbox']+label.u-no-margin--bottom.is-h4,[type='radio']+label.u-no-margin--bottom.is-h4,.u-no-margin--bottom.p-heading--4,.u-no-margin--bottom.p-heading--four,.u-no-margin--bottom.p-matrix__title,.u-no-margin--bottom.p-media-object__title,.u-no-margin--bottom.p-modal__title,.p-pull-quote .u-no-margin--bottom.p-pull-quote__quote,.u-no-margin--bottom.p-panel__title,.p-media-object__title{margin-bottom:.2rem !important}}@media (min-width: 772px){h4.u-no-margin--bottom,[type='checkbox']+label.u-no-margin--bottom.is-h4,[type='radio']+label.u-no-margin--bottom.is-h4,.u-no-margin--bottom.p-heading--4,.u-no-margin--bottom.p-heading--four,.u-no-margin--bottom.p-matrix__title,.u-no-margin--bottom.p-media-object__title,.u-no-margin--bottom.p-modal__title,.p-pull-quote .u-no-margin--bottom.p-pull-quote__quote,.u-no-margin--bottom.p-panel__title,.p-media-object__title{margin-bottom:-0.05rem !important}}@media (min-width: 1681px){h4.u-no-margin--bottom,[type='checkbox']+label.u-no-margin--bottom.is-h4,[type='radio']+label.u-no-margin--bottom.is-h4,.u-no-margin--bottom.p-heading--4,.u-no-margin--bottom.p-heading--four,.u-no-margin--bottom.p-matrix__title,.u-no-margin--bottom.p-media-object__title,.u-no-margin--bottom.p-modal__title,.p-pull-quote .u-no-margin--bottom.p-pull-quote__quote,.u-no-margin--bottom.p-panel__title,.p-media-object__title{margin-bottom:-0 !important}}h5.u-no-margin--bottom,.p-code-snippet .u-no-margin--bottom.p-code-snippet__title,.u-no-margin--bottom.p-heading--5,.u-no-margin--bottom.p-heading--five,h6.u-no-margin--bottom,.u-no-margin--bottom.p-heading--6,.u-no-margin--bottom.p-heading--six,.p-pull-quote .u-no-margin--bottom.p-pull-quote__citation,.p-pull-quote--small .u-no-margin--bottom.p-pull-quote__citation,.p-pull-quote--large .u-no-margin--bottom.p-pull-quote__citation,p.u-no-margin--bottom,.p-card__content,.p-notification__response{margin-bottom:.1rem !important}[type='checkbox']+label.u-no-margin--bottom.is-muted-heading,[type='radio']+label.u-no-margin--bottom.is-muted-heading,[type='checkbox']+label.u-no-margin--bottom.is-muted-inline-heading,[type='radio']+label.u-no-margin--bottom.is-muted-inline-heading,.u-no-margin--bottom.p-muted-heading,.p-table--mobile-card td.u-no-margin--bottom::before,.p-table--mobile-card tbody th.u-no-margin--bottom::before,small.u-no-margin--bottom,.u-no-margin--bottom.p-text--small,.u-no-margin--bottom.p-chip,.u-no-margin--bottom.p-chip__value,.u-no-margin--bottom.p-form-help-text,.u-no-margin--bottom.p-form-validation__message,.u-no-margin--bottom.p-media-object__meta-list-item--date,.u-no-margin--bottom.p-media-object__meta-list-item--location,.u-no-margin--bottom.p-media-object__meta-list-item--venue,.u-no-margin--bottom.p-media-object__meta-list-item,.u-no-margin--bottom.p-tooltip__message,[type='checkbox']+label.u-no-margin--bottom.is-table-header,[type='radio']+label.u-no-margin--bottom.is-table-header,thead th.u-no-margin--bottom,.u-no-margin--bottom.p-text--x-small,.u-no-margin--bottom.p-text--x-small-capitalized,.u-no-margin--bottom.p-chip__lead,.u-no-margin--bottom.p-label--validated,.u-no-margin--bottom.p-label--in-progress,.u-no-margin--bottom.p-label--new,.u-no-margin--bottom.p-label--updated,.u-no-margin--bottom.p-label--deprecated,.p-label--validated,.p-label--in-progress,.p-label--new,.p-label--updated,.p-label--deprecated{margin-bottom:-0.2rem !important}hr.u-no-margin--bottom{margin-bottom:-1px !important}.p-code-copyable::before,.p-code-snippet .p-code-snippet__block--icon::before,.p-icon--anchor,.p-icon--plus,.p-icon--minus,.p-icon--expand,.p-icon--collapse,.p-icon--chevron-down,.p-icon--chevron-up,.p-icon--contextual-menu,.p-icon--close,.p-icon--help,.p-icon--question,.p-icon--information,.p-icon--delete,.p-icon--error,.p-icon--warning,.p-icon--external-link,.p-icon--drag,.p-icon--code,.p-icon--menu,.p-icon--copy,.p-icon--search,.p-icon--success,.p-icon--share,.p-icon--user,.p-icon--spinner,.p-side-navigation__toggle::before,.p-side-navigation__toggle--in-drawer::before{background-size:contain;height:1rem;width:1rem;background-position:center;background-repeat:no-repeat;display:inline-block;font-size:inherit;margin:0;padding:0;position:relative;vertical-align:calc(.5px + 0.3465em - 0.5rem)}.p-icon--facebook,.p-icon--twitter,.p-icon--instagram,.p-icon--linkedin,.p-icon--youtube,.p-icon--canonical,.p-icon--ubuntu,.p-icon--rss,.p-icon--email{background-size:contain;height:2rem;width:2rem;display:inline-block}html{background:#fff}@font-face{font-display:fallback;font-family:'Ubuntu';font-style:normal;font-weight:300;src:url("https://assets.ubuntu.com/v1/e8c07df6-Ubuntu-L_W.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/8619add2-Ubuntu-L_W.woff") format("woff")}@font-face{font-display:fallback;font-family:'Ubuntu';font-style:normal;font-weight:400;src:url("https://assets.ubuntu.com/v1/fff37993-Ubuntu-R_W.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/7af50859-Ubuntu-R_W.woff") format("woff")}@font-face{font-display:fallback;font-family:'Ubuntu';font-style:italic;font-weight:300;src:url("https://assets.ubuntu.com/v1/f8097dea-Ubuntu-LI_W.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/8be89d02-Ubuntu-LI_W.woff") format("woff")}@font-face{font-display:fallback;font-family:'Ubuntu';font-style:italic;font-weight:400;src:url("https://assets.ubuntu.com/v1/fca66073-ubuntu-ri-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/f0898c72-ubuntu-ri-webfont.woff") format("woff")}@font-face{font-display:fallback;font-family:'Ubuntu';font-style:normal;font-weight:100;src:url("https://assets.ubuntu.com/v1/7f100985-Ubuntu-Th_W.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/502cc3a1-Ubuntu-Th_W.woff") format("woff")}@font-face{font-display:fallback;font-family:'Ubuntu Mono';font-style:normal;font-weight:300;src:url("https://assets.ubuntu.com/v1/fdd692b9-UbuntuMono-R_W.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/85edb898-UbuntuMono-R_W.woff") format("woff")}@font-face{font-display:fallback;font-family:'Ubuntu Mono';font-style:normal;font-weight:400;src:url("https://assets.ubuntu.com/v1/dd4acb63-UbuntuMono-B.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/e8e36b19-UbuntuMono-B.woff") format("woff")}.measure--p{max-width:40em}html{color:#111;font-family:"Ubuntu", -apple-system, "Segoe UI", "Roboto", "Oxygen", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-weight:300;line-height:1.5rem}@media screen and (max-width: 1681px){html{font-size:1rem}}@media screen and (min-width: 1681px){html{font-size:1.125rem;line-height:1.6875rem}}p{max-width:40em}small.dense,.p-text--small.dense{margin-bottom:1.3rem}.p-text--x-small-capitalized{font-weight:400;text-transform:uppercase}p:not([class*='p-heading--']):not([class*='p-muted-heading']):empty{line-height:0;margin:0;padding:0}sub,sup{line-height:0;position:relative;vertical-align:baseline}abbr[title]{border-bottom:0.1em dotted;cursor:help;text-decoration:none}blockquote{border-left:2px solid #666;margin-bottom:1rem;margin-left:0;margin-top:0;overflow:auto;padding-bottom:.5rem;padding-left:1.5rem}blockquote>:last-child{margin-bottom:.1rem}blockquote>cite{display:block;font-style:normal}html{box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}button{background-color:#fff;border-color:rgba(0,0,0,0.56);color:#111}button:visited{color:#111}button:hover{background-color:#f2f2f2;border-color:rgba(0,0,0,0.56)}button:active{background-color:#d9d9d9;border-color:rgba(0,0,0,0.56);transition-duration:0s}button:disabled:active,button:disabled:hover,button.is-disabled:active,button.is-disabled:hover{background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0.56)}button .p-link--external{color:currentColor}button,.p-button,.p-button--neutral,.p-button--brand,.p-button--positive,.p-button--negative,.p-button--base,.p-pagination__link,.p-pagination__link--previous,.p-pagination__link--next,.p-side-navigation__toggle,.p-side-navigation__toggle--in-drawer{transition-duration:0.165s;transition-property:background-color,border-color;transition-timing-function:cubic-bezier(0.55, 0.055, 0.675, 0.19);border-radius:.125rem;border-style:solid;border-width:1px;cursor:pointer;display:inline-block;font-size:1rem;font-weight:300;justify-content:center;line-height:1.5rem;margin:0 0 1.2rem 0;padding:calc(.4rem - 1px) 1rem;text-align:center;text-decoration:none}button:focus,.p-button:focus,.p-button--neutral:focus,.p-button--brand:focus,.p-button--positive:focus,.p-button--negative:focus,.p-button--base:focus,.p-pagination__link:focus,.p-pagination__link--previous:focus,.p-pagination__link--next:focus,.p-side-navigation__toggle:focus,.p-side-navigation__toggle--in-drawer:focus{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}button:focus-visible,.p-button:focus-visible,.p-button--neutral:focus-visible,.p-button--brand:focus-visible,.p-button--positive:focus-visible,.p-button--negative:focus-visible,.p-button--base:focus-visible,.p-pagination__link:focus-visible,.p-pagination__link--previous:focus-visible,.p-pagination__link--next:focus-visible,.p-side-navigation__toggle:focus-visible,.p-side-navigation__toggle--in-drawer:focus-visible{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}button:focus:not(:focus-visible),.p-button:focus:not(:focus-visible),.p-button--neutral:focus:not(:focus-visible),.p-button--brand:focus:not(:focus-visible),.p-button--positive:focus:not(:focus-visible),.p-button--negative:focus:not(:focus-visible),.p-button--base:focus:not(:focus-visible),.p-pagination__link:focus:not(:focus-visible),.p-pagination__link--previous:focus:not(:focus-visible),.p-pagination__link--next:focus:not(:focus-visible),.p-side-navigation__toggle:focus:not(:focus-visible),.p-side-navigation__toggle--in-drawer:focus:not(:focus-visible){outline:0;outline-offset:0}button:active,.p-button:active,.p-button--neutral:active,.p-button--brand:active,.p-button--positive:active,.p-button--negative:active,.p-button--base:active,.p-pagination__link:active,.p-pagination__link--previous:active,.p-pagination__link--next:active,.p-side-navigation__toggle:active,.p-side-navigation__toggle--in-drawer:active,button:focus,.p-button:focus,.p-button--neutral:focus,.p-button--brand:focus,.p-button--positive:focus,.p-button--negative:focus,.p-button--base:focus,.p-pagination__link:focus,.p-pagination__link--previous:focus,.p-pagination__link--next:focus,.p-side-navigation__toggle:focus,.p-side-navigation__toggle--in-drawer:focus,button:hover,.p-button:hover,.p-button--neutral:hover,.p-button--brand:hover,.p-button--positive:hover,.p-button--negative:hover,.p-button--base:hover,.p-pagination__link:hover,.p-pagination__link--previous:hover,.p-pagination__link--next:hover,.p-side-navigation__toggle:hover,.p-side-navigation__toggle--in-drawer:hover{text-decoration:none}button:disabled,.p-button:disabled,.p-button--neutral:disabled,.p-button--brand:disabled,.p-button--positive:disabled,.p-button--negative:disabled,.p-button--base:disabled,.p-pagination__link:disabled,.p-pagination__link--previous:disabled,.p-pagination__link--next:disabled,.p-side-navigation__toggle:disabled,.p-side-navigation__toggle--in-drawer:disabled,button.is-disabled,.is-disabled.p-button,.is-disabled.p-button--neutral,.is-disabled.p-button--brand,.is-disabled.p-button--positive,.is-disabled.p-button--negative,.is-disabled.p-button--base,.is-disabled.p-pagination__link,.is-disabled.p-pagination__link--previous,.is-disabled.p-pagination__link--next,.is-disabled.p-side-navigation__toggle,.is-disabled.p-side-navigation__toggle--in-drawer{cursor:not-allowed;opacity:.33}@media only screen and (max-width: 460px){button,.p-button,.p-button--neutral,.p-button--brand,.p-button--positive,.p-button--negative,.p-button--base,.p-pagination__link,.p-pagination__link--previous,.p-pagination__link--next,.p-side-navigation__toggle,.p-side-navigation__toggle--in-drawer{width:100%}}@media only screen and (min-width: 461px){button,.p-button,.p-button--neutral,.p-button--brand,.p-button--positive,.p-button--negative,.p-button--base,.p-pagination__link,.p-pagination__link--previous,.p-pagination__link--next,.p-side-navigation__toggle,.p-side-navigation__toggle--in-drawer{width:auto}button:not(:last-of-type):not(:only-of-type),.p-button:not(:last-of-type):not(:only-of-type),.p-button--neutral:not(:last-of-type):not(:only-of-type),.p-button--brand:not(:last-of-type):not(:only-of-type),.p-button--positive:not(:last-of-type):not(:only-of-type),.p-button--negative:not(:last-of-type):not(:only-of-type),.p-button--base:not(:last-of-type):not(:only-of-type),.p-pagination__link:not(:last-of-type):not(:only-of-type),.p-pagination__link--previous:not(:last-of-type):not(:only-of-type),.p-pagination__link--next:not(:last-of-type):not(:only-of-type),.p-side-navigation__toggle:not(:last-of-type):not(:only-of-type),.p-side-navigation__toggle--in-drawer:not(:last-of-type):not(:only-of-type){margin-right:1rem}}button.is-dense,.is-dense.p-button,.is-dense.p-button--neutral,.is-dense.p-button--brand,.is-dense.p-button--positive,.is-dense.p-button--negative,.is-dense.p-button--base,.is-dense.p-pagination__link,.is-dense.p-pagination__link--previous,.is-dense.p-pagination__link--next,.is-dense.p-side-navigation__toggle,.is-dense.p-side-navigation__toggle--in-drawer{margin-bottom:.1rem;padding-bottom:calc(.15rem - 1px);padding-top:calc(.15rem - 1px)}button.is-small,.is-small.p-button,.is-small.p-button--neutral,.is-small.p-button--brand,.is-small.p-button--positive,.is-small.p-button--negative,.is-small.p-button--base,.is-small.p-pagination__link,.is-small.p-pagination__link--previous,.is-small.p-pagination__link--next,.is-small.p-side-navigation__toggle,.is-small.p-side-navigation__toggle--in-drawer{font-size:.875rem;line-height:1rem;margin:0 0 .7rem 0;padding:calc(0.2rem - 1px) .5rem}button.is-small.is-dense,.is-small.is-dense.p-button,.is-small.is-dense.p-button--neutral,.is-small.is-dense.p-button--brand,.is-small.is-dense.p-button--positive,.is-small.is-dense.p-button--negative,.is-small.is-dense.p-button--base,.is-small.is-dense.p-pagination__link,.is-small.is-dense.p-pagination__link--previous,.is-small.is-dense.p-pagination__link--next,.is-small.is-dense.p-side-navigation__toggle,.is-small.is-dense.p-side-navigation__toggle--in-drawer{margin-bottom:.1rem;padding-bottom:calc(.15rem - 1px);padding-top:calc(.15rem - 1px)}p button,p .p-button,p .p-button--neutral,p .p-button--brand,p .p-button--positive,p .p-button--negative,p .p-button--base,p .p-pagination__link,p .p-pagination__link--previous,p .p-pagination__link--next,p .p-side-navigation__toggle,p .p-side-navigation__toggle--in-drawer{margin-bottom:.6rem;margin-top:-.4rem}p+p>button,p+p>.p-button,p+p>.p-button--neutral,p+p>.p-button--brand,p+p>.p-button--positive,p+p>.p-button--negative,p+p>.p-button--base,p+p>.p-pagination__link,p+p>.p-pagination__link--previous,p+p>.p-pagination__link--next,p+p>.p-side-navigation__toggle,p+p>.p-side-navigation__toggle--in-drawer{margin-top:.1rem}@media only screen and (max-width: 460px){p button+button,p .p-button+button,p .p-button--neutral+button,p .p-button--brand+button,p .p-button--positive+button,p .p-button--negative+button,p .p-button--base+button,p .p-pagination__link+button,p .p-pagination__link--previous+button,p .p-pagination__link--next+button,p .p-side-navigation__toggle+button,p .p-side-navigation__toggle--in-drawer+button,p button+.p-button,p .p-button+.p-button,p .p-button--neutral+.p-button,p .p-button--brand+.p-button,p .p-button--positive+.p-button,p .p-button--negative+.p-button,p .p-button--base+.p-button,p .p-pagination__link+.p-button,p .p-pagination__link--previous+.p-button,p .p-pagination__link--next+.p-button,p .p-side-navigation__toggle+.p-button,p .p-side-navigation__toggle--in-drawer+.p-button,p button+.p-button--neutral,p .p-button+.p-button--neutral,p .p-button--neutral+.p-button--neutral,p .p-button--brand+.p-button--neutral,p .p-button--positive+.p-button--neutral,p .p-button--negative+.p-button--neutral,p .p-button--base+.p-button--neutral,p .p-pagination__link+.p-button--neutral,p .p-pagination__link--previous+.p-button--neutral,p .p-pagination__link--next+.p-button--neutral,p .p-side-navigation__toggle+.p-button--neutral,p .p-side-navigation__toggle--in-drawer+.p-button--neutral,p button+.p-button--brand,p .p-button+.p-button--brand,p .p-button--neutral+.p-button--brand,p .p-button--brand+.p-button--brand,p .p-button--positive+.p-button--brand,p .p-button--negative+.p-button--brand,p .p-button--base+.p-button--brand,p .p-pagination__link+.p-button--brand,p .p-pagination__link--previous+.p-button--brand,p .p-pagination__link--next+.p-button--brand,p .p-side-navigation__toggle+.p-button--brand,p .p-side-navigation__toggle--in-drawer+.p-button--brand,p button+.p-button--positive,p .p-button+.p-button--positive,p .p-button--neutral+.p-button--positive,p .p-button--brand+.p-button--positive,p .p-button--positive+.p-button--positive,p .p-button--negative+.p-button--positive,p .p-button--base+.p-button--positive,p .p-pagination__link+.p-button--positive,p .p-pagination__link--previous+.p-button--positive,p .p-pagination__link--next+.p-button--positive,p .p-side-navigation__toggle+.p-button--positive,p .p-side-navigation__toggle--in-drawer+.p-button--positive,p button+.p-button--negative,p .p-button+.p-button--negative,p .p-button--neutral+.p-button--negative,p .p-button--brand+.p-button--negative,p .p-button--positive+.p-button--negative,p .p-button--negative+.p-button--negative,p .p-button--base+.p-button--negative,p .p-pagination__link+.p-button--negative,p .p-pagination__link--previous+.p-button--negative,p .p-pagination__link--next+.p-button--negative,p .p-side-navigation__toggle+.p-button--negative,p .p-side-navigation__toggle--in-drawer+.p-button--negative,p button+.p-button--base,p .p-button+.p-button--base,p .p-button--neutral+.p-button--base,p .p-button--brand+.p-button--base,p .p-button--positive+.p-button--base,p .p-button--negative+.p-button--base,p .p-button--base+.p-button--base,p .p-pagination__link+.p-button--base,p .p-pagination__link--previous+.p-button--base,p .p-pagination__link--next+.p-button--base,p .p-side-navigation__toggle+.p-button--base,p .p-side-navigation__toggle--in-drawer+.p-button--base,p button+.p-pagination__link,p .p-button+.p-pagination__link,p .p-button--neutral+.p-pagination__link,p .p-button--brand+.p-pagination__link,p .p-button--positive+.p-pagination__link,p .p-button--negative+.p-pagination__link,p .p-button--base+.p-pagination__link,p .p-pagination__link+.p-pagination__link,p .p-pagination__link--previous+.p-pagination__link,p .p-pagination__link--next+.p-pagination__link,p .p-side-navigation__toggle+.p-pagination__link,p .p-side-navigation__toggle--in-drawer+.p-pagination__link,p button+.p-pagination__link--previous,p .p-button+.p-pagination__link--previous,p .p-button--neutral+.p-pagination__link--previous,p .p-button--brand+.p-pagination__link--previous,p .p-button--positive+.p-pagination__link--previous,p .p-button--negative+.p-pagination__link--previous,p .p-button--base+.p-pagination__link--previous,p .p-pagination__link+.p-pagination__link--previous,p .p-pagination__link--previous+.p-pagination__link--previous,p .p-pagination__link--next+.p-pagination__link--previous,p .p-side-navigation__toggle+.p-pagination__link--previous,p .p-side-navigation__toggle--in-drawer+.p-pagination__link--previous,p button+.p-pagination__link--next,p .p-button+.p-pagination__link--next,p .p-button--neutral+.p-pagination__link--next,p .p-button--brand+.p-pagination__link--next,p .p-button--positive+.p-pagination__link--next,p .p-button--negative+.p-pagination__link--next,p .p-button--base+.p-pagination__link--next,p .p-pagination__link+.p-pagination__link--next,p .p-pagination__link--previous+.p-pagination__link--next,p .p-pagination__link--next+.p-pagination__link--next,p .p-side-navigation__toggle+.p-pagination__link--next,p .p-side-navigation__toggle--in-drawer+.p-pagination__link--next,p button+.p-side-navigation__toggle,p .p-button+.p-side-navigation__toggle,p .p-button--neutral+.p-side-navigation__toggle,p .p-button--brand+.p-side-navigation__toggle,p .p-button--positive+.p-side-navigation__toggle,p .p-button--negative+.p-side-navigation__toggle,p .p-button--base+.p-side-navigation__toggle,p .p-pagination__link+.p-side-navigation__toggle,p .p-pagination__link--previous+.p-side-navigation__toggle,p .p-pagination__link--next+.p-side-navigation__toggle,p .p-side-navigation__toggle+.p-side-navigation__toggle,p .p-side-navigation__toggle--in-drawer+.p-side-navigation__toggle,p button+.p-side-navigation__toggle--in-drawer,p .p-button+.p-side-navigation__toggle--in-drawer,p .p-button--neutral+.p-side-navigation__toggle--in-drawer,p .p-button--brand+.p-side-navigation__toggle--in-drawer,p .p-button--positive+.p-side-navigation__toggle--in-drawer,p .p-button--negative+.p-side-navigation__toggle--in-drawer,p .p-button--base+.p-side-navigation__toggle--in-drawer,p .p-pagination__link+.p-side-navigation__toggle--in-drawer,p .p-pagination__link--previous+.p-side-navigation__toggle--in-drawer,p .p-pagination__link--next+.p-side-navigation__toggle--in-drawer,p .p-side-navigation__toggle+.p-side-navigation__toggle--in-drawer,p .p-side-navigation__toggle--in-drawer+.p-side-navigation__toggle--in-drawer{margin-top:.6rem}}.p-button--brand .p-icon--success,.p-button--positive .p-icon--success,.p-button--negative .p-icon--success{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cg fill='none' fill-rule='nonzero'%3E%3Cpath fill='%23fff' d='M8 1a7 7 0 110 14A7 7 0 018 1zm2.83 3.502L6.863 9.884 5.174 8.096l-1.09 1.03 2.92 3.096 5.034-6.83-1.208-.89z'/%3E%3Cpath fill='rgba(0%2C0%2C0%2C0)' d='M10.83 4.502l1.208.89-5.033 6.83-2.922-3.096 1.091-1.03 1.689 1.789z'/%3E%3C/g%3E%3C/svg%3E")}code,kbd,pre,samp{font-family:"Ubuntu Mono", Consolas, Monaco, Courier, monospace;font-weight:300;text-align:left}code b,code strong,kbd b,kbd strong,pre b,pre strong,samp b,samp strong{font-weight:bold}code,kbd,samp{background-color:rgba(0,0,0,0.03);border-radius:.125rem;-webkit-box-decoration-break:slice;box-decoration-break:slice;color:inherit;line-height:1.25rem;padding:calc(.25rem - 1px) .25rem}code,pre{direction:ltr;-webkit-hyphens:none;-ms-hyphens:none;hyphens:none;-moz-tab-size:4;-o-tab-size:4;tab-size:4;word-spacing:normal;word-wrap:break-word}pre code{background:none;box-shadow:none;display:inline-block;line-height:1.5rem;margin-left:0;margin-right:0;padding:0}pre{background-color:rgba(0,0,0,0.03);color:#111;display:block;margin-bottom:1.5rem;margin-top:0;overflow:auto;padding:.5rem 1rem;text-align:left;text-shadow:none;white-space:pre}[class*='--dark'] code,code.is-dark{background-color:rgba(255,255,255,0.3);color:#fff}.p-code-copyable::before,.p-code-snippet .p-code-snippet__block--icon::before{background-image:url("data:image/svg+xml,%3Csvg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg'%3E%3Cg id='dollar-sign' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cpath d='M8.85063291,15 L8.85063291,13.1075949 C9.73924051,12.9978903 10.3974684,12.7181434 10.8253165,12.2683544 C11.2531646,11.8185654 11.4670886,11.2700422 11.4670886,10.6227848 C11.4670886,10.1949367 11.3875528,9.82742616 11.228481,9.52025316 C11.0694092,9.21308017 10.8582278,8.94978903 10.5949367,8.73037975 C10.3316456,8.51097047 10.0244726,8.32172996 9.67341772,8.16265823 C9.32236287,8.00358649 8.96033756,7.85274262 8.58734177,7.71012658 C8.29113924,7.60042194 8.02510548,7.49620253 7.78924051,7.39746835 C7.55337553,7.29873418 7.34767933,7.18902953 7.1721519,7.06835443 C6.99662447,6.94767933 6.86223629,6.81054852 6.76898734,6.65696203 C6.67573839,6.50337553 6.62911392,6.31139241 6.62911392,6.08101266 C6.62911392,5.66413502 6.78544304,5.34599156 7.09810127,5.12658228 C7.41075949,4.907173 7.86329114,4.79746835 8.4556962,4.79746835 C8.99324895,4.79746835 9.43755275,4.84409282 9.78860759,4.93734177 C10.1396624,5.03059072 10.4303797,5.12109705 10.6607595,5.20886076 L10.9405063,4.05696203 C10.7210971,3.96919832 10.4276371,3.88417722 10.0601266,3.80189873 C9.69261604,3.71962025 9.28945148,3.66751055 8.85063291,3.64556962 L8.85063291,2 L7.63291139,2 L7.63291139,3.69493671 C6.84303797,3.81561181 6.23966244,4.09535865 5.82278481,4.53417722 C5.40590718,4.97299578 5.19746835,5.54345991 5.19746835,6.24556962 C5.19746835,6.64050633 5.26877637,6.97236287 5.41139241,7.24113924 C5.55400844,7.50991561 5.73776371,7.74029536 5.96265823,7.93227848 C6.18755275,8.12426161 6.44535865,8.28607595 6.73607595,8.41772152 C7.02679325,8.54936709 7.32025316,8.67004219 7.6164557,8.77974684 C7.9236287,8.88945148 8.21983123,9.00189873 8.50506329,9.11708861 C8.79029536,9.23227848 9.04535865,9.36392405 9.27025316,9.51202532 C9.49514768,9.66012658 9.67341772,9.83016877 9.80506329,10.0221519 C9.93670886,10.214135 10.0025316,10.4472574 10.0025316,10.721519 C10.0025316,10.9080169 9.96962025,11.0808017 9.90379747,11.2398734 C9.83797468,11.3989452 9.72827004,11.5360759 9.57468354,11.6512658 C9.42109705,11.7664557 9.20991561,11.8542194 8.94113924,11.914557 C8.67236287,11.9748945 8.34050633,12.0050633 7.94556962,12.0050633 C7.33122363,12.0050633 6.82109705,11.9419832 6.41518987,11.8158228 C6.0092827,11.6896624 5.65822785,11.5552743 5.36202532,11.4126582 L5,12.5481013 C5.20843882,12.6687763 5.52658228,12.7949367 5.95443038,12.9265823 C6.38227848,13.0582278 6.94177215,13.1350211 7.63291139,13.156962 L7.63291139,15 L8.85063291,15 Z' id='$' fill='%23666' fill-rule='nonzero'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");content:'';left:1rem;position:absolute;top:.75rem;width:1rem}@media (min-width: 1681px){.p-code-copyable::before,.p-code-snippet .p-code-snippet__block--icon::before{top:.84375rem}}.p-code-numbered__line,.p-code-snippet .p-code-snippet__line{display:inline-block;padding:0 1rem 0 calc(1rem + (4 * 1ch));position:relative;width:100%}.p-code-numbered__line:empty::after,.p-code-snippet .p-code-snippet__line:empty::after{content:' ';-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.p-code-numbered__line::before,.p-code-snippet .p-code-snippet__line::before{color:#666;content:counter(line-numbering);counter-increment:line-numbering;height:100%;left:-1ch;overflow:hidden;padding-right:1rem;pointer-events:none;position:absolute;text-align:right;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:calc(calc(1rem + (4 * 1ch)) + 1ch)}details{margin-bottom:1.5rem;overflow:auto}summary{max-width:40em;margin-bottom:.4rem;padding-bottom:.6rem}summary:focus{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}summary:focus-visible{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}summary:focus:not(:focus-visible){outline:0;outline-offset:0}[type='checkbox'],[type='radio'],.p-checkbox__input[type='checkbox'],.p-radio__input[type='radio']{float:none;height:1rem;margin:0;opacity:0;position:absolute;width:1rem}[type='checkbox']+label,[type='radio']+label,.p-checkbox__label,.p-radio__label{padding-left:2rem;position:relative}[type='checkbox']+label::before,[type='radio']+label::before,.p-checkbox__label::before,.p-radio__label::before,[type='checkbox']+label::after,[type='radio']+label::after,.p-checkbox__label::after,.p-radio__label::after{transition-duration:0.333s;transition-property:background-color,border-color;transition-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1);position:absolute}[type='checkbox']+label::before,[type='radio']+label::before,.p-checkbox__label::before,.p-radio__label::before{content:'';height:1rem;left:0;outline-offset:1px;top:calc(.932em - 1rem + .15rem);width:1rem}[type='checkbox']+label::after,[type='radio']+label::after,.p-checkbox__label::after,.p-radio__label::after{content:'';opacity:0}:checked[type='checkbox']+label::before,:checked[type='radio']+label::before,.p-checkbox__input:checked+.p-checkbox__label::before,.p-radio__input:checked+.p-radio__label::before{background-color:#06c;border-color:#06c}:checked[type='checkbox']+label::after,:checked[type='radio']+label::after,.p-checkbox__input:checked+.p-checkbox__label::after,.p-radio__input:checked+.p-radio__label::after{opacity:1}:focus[type='checkbox']+label::before,:focus[type='radio']+label::before,.p-checkbox__input:focus+.p-checkbox__label::before,.p-radio__input:focus+.p-radio__label::before{outline:.1875rem solid #2e96ff}[type='checkbox']+label::before,.p-checkbox__label::before{border-radius:.125rem}[type='checkbox']+label::after,.p-checkbox__label::after{border-bottom:2px solid;border-left:2px solid;height:.375rem;left:.1875rem;top:calc(.932em - 1rem + .1875rem + .15rem);transform:rotate(-45deg);width:0.625rem}[type='radio']+label::before,.p-radio__label::before{border-radius:50%}[type='radio']+label::after,.p-radio__label::after{border-radius:50%;height:.375rem;left:.3125rem;top:calc(.932em - 1rem + .3125rem + .15rem);width:.375rem}label [type='checkbox'],label [type='radio']{float:left;margin:.25rem 1rem 0 0;opacity:1;position:static}@media (max-width: 772px){[type='checkbox']+label:not(.is-h1)::before,[type='radio']+label:not(.is-h1)::before,[type='checkbox']+label:not(.is-h2)::before,[type='radio']+label:not(.is-h2)::before,[type='checkbox']+label:not(.is-h3)::before,[type='radio']+label:not(.is-h3)::before,[type='checkbox']+label:not(.is-h4)::before,[type='radio']+label:not(.is-h4)::before,[type='checkbox']+label:not(.is-muted-heading)::before,[type='radio']+label:not(.is-muted-heading)::before,[type='checkbox']+label:not(.is-inline-label)::before,[type='radio']+label:not(.is-inline-label)::before{top:.6665rem}[type='checkbox']+label.is-h1::before,[type='radio']+label.is-h1::before{top:1.5rem}[type='checkbox']+label.is-h2::before,[type='radio']+label.is-h2::before{top:1rem}[type='checkbox']+label.is-h3::before,[type='radio']+label.is-h3::before{top:1rem}[type='checkbox']+label.is-h4::before,[type='radio']+label.is-h4::before{top:.5rem}[type='checkbox']+label.is-inline-label::before,[type='radio']+label.is-inline-label::before{top:.0625rem}[type='checkbox']+label.is-muted-heading::before,[type='radio']+label.is-muted-heading::before{top:.1665rem}[type='checkbox']+label.is-muted-inline-heading::before,[type='radio']+label.is-muted-inline-heading::before{top:0rem}[type='checkbox']+label.is-table-header::before,[type='radio']+label.is-table-header::before{top:-.05rem}}@media (min-width: 772px){[type='checkbox']+label:not(.is-h1)::before,[type='radio']+label:not(.is-h1)::before,[type='checkbox']+label:not(.is-h2)::before,[type='radio']+label:not(.is-h2)::before,[type='checkbox']+label:not(.is-h3)::before,[type='radio']+label:not(.is-h3)::before,[type='checkbox']+label:not(.is-h4)::before,[type='radio']+label:not(.is-h4)::before,[type='checkbox']+label:not(.is-muted-heading)::before,[type='radio']+label:not(.is-muted-heading)::before,[type='checkbox']+label:not(.is-inline-label)::before,[type='radio']+label:not(.is-inline-label)::before{top:.6665rem}[type='checkbox']+label.is-h1::before,[type='radio']+label.is-h1::before{top:2rem}[type='checkbox']+label.is-h2::before,[type='radio']+label.is-h2::before{top:1.5rem}[type='checkbox']+label.is-h3::before,[type='radio']+label.is-h3::before{top:1rem}[type='checkbox']+label.is-h4::before,[type='radio']+label.is-h4::before{top:.5rem}[type='checkbox']+label.is-inline-label::before,[type='radio']+label.is-inline-label::before{top:.0625rem}[type='checkbox']+label.is-muted-heading::before,[type='radio']+label.is-muted-heading::before{top:.1665rem}[type='checkbox']+label.is-muted-inline-heading::before,[type='radio']+label.is-muted-inline-heading::before{top:0rem}[type='checkbox']+label.is-table-header::before,[type='radio']+label.is-table-header::before{top:-.05rem}}[type='checkbox']+label.is-inline-label,[type='radio']+label.is-inline-label{display:inline;padding-top:0}[type='checkbox']+label.is-muted-inline-heading,[type='radio']+label.is-muted-inline-heading{display:inline;padding-top:0}[type='checkbox']+label.is-table-header,[type='radio']+label.is-table-header{display:inline;padding-top:0}@media (max-width: 772px){[type='checkbox']+label:not(.is-h1)::after,[type='checkbox']+label:not(.is-h2)::after,[type='checkbox']+label:not(.is-h3)::after,[type='checkbox']+label:not(.is-h4)::after,[type='checkbox']+label:not(.is-muted-heading)::after,[type='checkbox']+label:not(.is-inline-label)::after{top:.854rem}[type='checkbox']+label.is-h1::after{top:1.6875rem}[type='checkbox']+label.is-h2::after{top:1.1875rem}[type='checkbox']+label.is-h3::after{top:1.1875rem}[type='checkbox']+label.is-h4::after{top:.6875rem}[type='checkbox']+label.is-inline-label::after{top:.25rem}[type='checkbox']+label.is-muted-heading::after{top:.354rem}[type='checkbox']+label.is-muted-inline-heading::after{top:.1875rem}[type='checkbox']+label.is-table-header::after{top:.1375rem}}@media (min-width: 772px){[type='checkbox']+label:not(.is-h1)::after,[type='checkbox']+label:not(.is-h2)::after,[type='checkbox']+label:not(.is-h3)::after,[type='checkbox']+label:not(.is-h4)::after,[type='checkbox']+label:not(.is-muted-heading)::after,[type='checkbox']+label:not(.is-inline-label)::after{top:.854rem}[type='checkbox']+label.is-h1::after{top:2.1875rem}[type='checkbox']+label.is-h2::after{top:1.6875rem}[type='checkbox']+label.is-h3::after{top:1.1875rem}[type='checkbox']+label.is-h4::after{top:.6875rem}[type='checkbox']+label.is-inline-label::after{top:.25rem}[type='checkbox']+label.is-muted-heading::after{top:.354rem}[type='checkbox']+label.is-muted-inline-heading::after{top:.1875rem}[type='checkbox']+label.is-table-header::after{top:.1375rem}}@media (max-width: 772px){[type='radio']+label:not(.is-h1)::after,[type='radio']+label:not(.is-h2)::after,[type='radio']+label:not(.is-h3)::after,[type='radio']+label:not(.is-h4)::after,[type='radio']+label:not(.is-muted-heading)::after,[type='radio']+label:not(.is-inline-label)::after{top:.979rem}[type='radio']+label.is-h1::after{top:1.8125rem}[type='radio']+label.is-h2::after{top:1.3125rem}[type='radio']+label.is-h3::after{top:1.3125rem}[type='radio']+label.is-h4::after{top:.8125rem}[type='radio']+label.is-inline-label::after{top:.375rem}[type='radio']+label.is-muted-heading::after{top:.479rem}[type='radio']+label.is-muted-inline-heading::after{top:.3125rem}[type='radio']+label.is-table-header::after{top:.2625rem}}@media (min-width: 772px){[type='radio']+label:not(.is-h1)::after,[type='radio']+label:not(.is-h2)::after,[type='radio']+label:not(.is-h3)::after,[type='radio']+label:not(.is-h4)::after,[type='radio']+label:not(.is-muted-heading)::after,[type='radio']+label:not(.is-inline-label)::after{top:.979rem}[type='radio']+label.is-h1::after{top:2.3125rem}[type='radio']+label.is-h2::after{top:1.8125rem}[type='radio']+label.is-h3::after{top:1.3125rem}[type='radio']+label.is-h4::after{top:.8125rem}[type='radio']+label.is-inline-label::after{top:.375rem}[type='radio']+label.is-muted-heading::after{top:.479rem}[type='radio']+label.is-muted-inline-heading::after{top:.3125rem}[type='radio']+label.is-table-header::after{top:.2625rem}}[type='checkbox']+label{color:#111}[type='checkbox']+label::before{background:#fff;border:1px solid rgba(0,0,0,0.56)}[type='checkbox']+label::after{color:#fff}[type='radio']+label{color:#111}[type='radio']+label::before{background:#fff;border:1px solid rgba(0,0,0,0.56)}[type='radio']+label::after{background-color:#fff}[type='checkbox'].is-dark+label{color:#fff}[type='checkbox'].is-dark+label::before{background:#262626;border:1px solid rgba(255,255,255,0.4)}[type='checkbox'].is-dark+label::after{color:#fff}[type='radio'].is-dark+label{color:#fff}[type='radio'].is-dark+label::before{background:#262626;border:1px solid rgba(255,255,255,0.4)}[type='radio'].is-dark+label::after{background-color:#fff}input[type='range']{-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:1.1875rem;margin:.5rem 0;padding:0;width:100%}input[type='range']::-webkit-slider-runnable-track{border:1px solid rgba(0,0,0,0.56);border-radius:1.1875rem;height:6px}input[type='range']::-webkit-slider-thumb{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#fff;border:0;border-radius:.1875rem;box-shadow:0 0 .1875rem 1px rgba(0,0,0,0.2);height:24px;margin-top:-10.5px;width:24px}input[type='range']::-webkit-slider-thumb:hover{cursor:pointer}input[type='range']::-moz-range-track{background:#fff;border:1px solid rgba(0,0,0,0.56);border-radius:.1875rem;height:4px}input[type='range']::-moz-range-progress{background-color:#24598f;border-radius:.1875rem;height:4px}input[type='range']::-moz-range-thumb{background:#fff;border:0;border-radius:.1875rem;box-shadow:0 0 .1875rem 1px rgba(0,0,0,0.2);height:24px;width:24px}input[type='range']::-moz-range-thumb:hover{cursor:pointer}input[type='range']::-moz-focus-outer{border:0}input[type='range']::-ms-track{background:transparent;border-color:transparent;border-width:12px;color:transparent;height:6px;width:calc(100% - (24px / 2))}input[type='range']::-ms-fill-lower{background:#24598f;border:1px solid rgba(0,0,0,0.56);border-radius:.1875rem}input[type='range']::-ms-fill-upper{background:#fff;border:1px solid rgba(0,0,0,0.56);border-radius:.1875rem}input[type='range']::-ms-thumb{background:#fff;border:0;border-radius:.1875rem;box-shadow:0 0 .1875rem 1px rgba(0,0,0,0.2);height:24px;margin:0 .1875rem;width:24px}input[type='range']::-ms-thumb:hover{cursor:pointer}input[type='range']::-ms-tooltip{display:none}input[type='range']:focus{outline:none}input[type='range']:focus::-webkit-slider-thumb{outline:.1875rem solid #2e96ff}input[type='range']:focus::-moz-range-thumb{outline:.1875rem solid #2e96ff}input[type='range']:focus::-ms-thumb{outline:.1875rem solid #2e96ff}input[type='range']:disabled{opacity:0.5}[type='text'],[type='date'],[type='datetime'],[type='datetime-local'],[type='month'],[type='time'],[type='week'],[type='number'],[type='search'],[type='password'],[type='email'],[type='url'],[type='tel'],select,textarea,[type='file']{padding-bottom:calc(.4rem - 1px);padding-top:calc(.4rem - 1px)}[type='text'],[type='date'],[type='datetime'],[type='datetime-local'],[type='month'],[type='time'],[type='week'],[type='number'],[type='search'],[type='password'],[type='email'],[type='url'],[type='tel'],select,textarea{-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield;background-color:#fff;border:1px solid rgba(0,0,0,0.56);border-radius:0;box-shadow:inset 0 1px 1px rgba(0,0,0,0.12);color:#111;font-family:"Ubuntu", -apple-system, "Segoe UI", "Roboto", "Oxygen", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;font-size:1rem;font-weight:300;line-height:1.5rem;margin-bottom:1.2rem;min-width:10em;padding-left:.5rem;padding-right:.5rem;vertical-align:baseline;width:100%}:focus[type='text'],:focus[type='date'],:focus[type='datetime'],:focus[type='datetime-local'],:focus[type='month'],:focus[type='time'],:focus[type='week'],:focus[type='number'],:focus[type='search'],:focus[type='password'],:focus[type='email'],:focus[type='url'],:focus[type='tel'],select:focus,textarea:focus{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}:focus-visible[type='text'],:focus-visible[type='date'],:focus-visible[type='datetime'],:focus-visible[type='datetime-local'],:focus-visible[type='month'],:focus-visible[type='time'],:focus-visible[type='week'],:focus-visible[type='number'],:focus-visible[type='search'],:focus-visible[type='password'],:focus-visible[type='email'],:focus-visible[type='url'],:focus-visible[type='tel'],select:focus-visible,textarea:focus-visible{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}:focus:not(:focus-visible)[type='text'],:focus:not(:focus-visible)[type='date'],:focus:not(:focus-visible)[type='datetime'],:focus:not(:focus-visible)[type='datetime-local'],:focus:not(:focus-visible)[type='month'],:focus:not(:focus-visible)[type='time'],:focus:not(:focus-visible)[type='week'],:focus:not(:focus-visible)[type='number'],:focus:not(:focus-visible)[type='search'],:focus:not(:focus-visible)[type='password'],:focus:not(:focus-visible)[type='email'],:focus:not(:focus-visible)[type='url'],:focus:not(:focus-visible)[type='tel'],select:focus:not(:focus-visible),textarea:focus:not(:focus-visible){outline:0;outline-offset:0}.is-error :focus[type='text'],.is-error :focus[type='date'],.is-error :focus[type='datetime'],.is-error :focus[type='datetime-local'],.is-error :focus[type='month'],.is-error :focus[type='time'],.is-error :focus[type='week'],.is-error :focus[type='number'],.is-error :focus[type='search'],.is-error :focus[type='password'],.is-error :focus[type='email'],.is-error :focus[type='url'],.is-error :focus[type='tel'],.is-error select:focus,.is-error textarea:focus{outline-color:#c7162b}.is-caution :focus[type='text'],.is-caution :focus[type='date'],.is-caution :focus[type='datetime'],.is-caution :focus[type='datetime-local'],.is-caution :focus[type='month'],.is-caution :focus[type='time'],.is-caution :focus[type='week'],.is-caution :focus[type='number'],.is-caution :focus[type='search'],.is-caution :focus[type='password'],.is-caution :focus[type='email'],.is-caution :focus[type='url'],.is-caution :focus[type='tel'],.is-caution select:focus,.is-caution textarea:focus{outline-color:#f99b11}.is-success :focus[type='text'],.is-success :focus[type='date'],.is-success :focus[type='datetime'],.is-success :focus[type='datetime-local'],.is-success :focus[type='month'],.is-success :focus[type='time'],.is-success :focus[type='week'],.is-success :focus[type='number'],.is-success :focus[type='search'],.is-success :focus[type='password'],.is-success :focus[type='email'],.is-success :focus[type='url'],.is-success :focus[type='tel'],.is-success select:focus,.is-success textarea:focus{outline-color:#0e8620}.is-dense[type='text'],.is-dense[type='date'],.is-dense[type='datetime'],.is-dense[type='datetime-local'],.is-dense[type='month'],.is-dense[type='time'],.is-dense[type='week'],.is-dense[type='number'],.is-dense[type='search'],.is-dense[type='password'],.is-dense[type='email'],.is-dense[type='url'],.is-dense[type='tel'],select.is-dense,textarea.is-dense{margin:0 0 .1rem 0;padding-bottom:calc(.15rem - 1px);padding-top:calc(.15rem - 1px)}[type='text']::-webkit-placeholder,[type='date']::-webkit-placeholder,[type='datetime']::-webkit-placeholder,[type='datetime-local']::-webkit-placeholder,[type='month']::-webkit-placeholder,[type='time']::-webkit-placeholder,[type='week']::-webkit-placeholder,[type='number']::-webkit-placeholder,[type='search']::-webkit-placeholder,[type='password']::-webkit-placeholder,[type='email']::-webkit-placeholder,[type='url']::-webkit-placeholder,[type='tel']::-webkit-placeholder,select::-webkit-placeholder,textarea::-webkit-placeholder,[type='text']::-ms-placeholder,[type='date']::-ms-placeholder,[type='datetime']::-ms-placeholder,[type='datetime-local']::-ms-placeholder,[type='month']::-ms-placeholder,[type='time']::-ms-placeholder,[type='week']::-ms-placeholder,[type='number']::-ms-placeholder,[type='search']::-ms-placeholder,[type='password']::-ms-placeholder,[type='email']::-ms-placeholder,[type='url']::-ms-placeholder,[type='tel']::-ms-placeholder,select::-ms-placeholder,textarea::-ms-placeholder,:-ms-placeholder[type='text'],:-ms-placeholder[type='date'],:-ms-placeholder[type='datetime'],:-ms-placeholder[type='datetime-local'],:-ms-placeholder[type='month'],:-ms-placeholder[type='time'],:-ms-placeholder[type='week'],:-ms-placeholder[type='number'],:-ms-placeholder[type='search'],:-ms-placeholder[type='password'],:-ms-placeholder[type='email'],:-ms-placeholder[type='url'],:-ms-placeholder[type='tel'],select:-ms-placeholder,textarea:-ms-placeholder,[type='text']::placeholder,[type='date']::placeholder,[type='datetime']::placeholder,[type='datetime-local']::placeholder,[type='month']::placeholder,[type='time']::placeholder,[type='week']::placeholder,[type='number']::placeholder,[type='search']::placeholder,[type='password']::placeholder,[type='email']::placeholder,[type='url']::placeholder,[type='tel']::placeholder,select::placeholder,textarea::placeholder{color:#666;opacity:1}.has-error[type='text'],.has-error[type='date'],.has-error[type='datetime'],.has-error[type='datetime-local'],.has-error[type='month'],.has-error[type='time'],.has-error[type='week'],.has-error[type='number'],.has-error[type='search'],.has-error[type='password'],.has-error[type='email'],.has-error[type='url'],.has-error[type='tel'],select.has-error,textarea.has-error{border:1px solid #c7162b}.has-caution[type='text'],.has-caution[type='date'],.has-caution[type='datetime'],.has-caution[type='datetime-local'],.has-caution[type='month'],.has-caution[type='time'],.has-caution[type='week'],.has-caution[type='number'],.has-caution[type='search'],.has-caution[type='password'],.has-caution[type='email'],.has-caution[type='url'],.has-caution[type='tel'],select.has-caution,textarea.has-caution{border:1px solid #f99b11}.has-success[type='text'],.has-success[type='date'],.has-success[type='datetime'],.has-success[type='datetime-local'],.has-success[type='month'],.has-success[type='time'],.has-success[type='week'],.has-success[type='number'],.has-success[type='search'],.has-success[type='password'],.has-success[type='email'],.has-success[type='url'],.has-success[type='tel'],select.has-success,textarea.has-success{border:1px solid #0e8620}.has-information[type='text'],.has-information[type='date'],.has-information[type='datetime'],.has-information[type='datetime-local'],.has-information[type='month'],.has-information[type='time'],.has-information[type='week'],.has-information[type='number'],.has-information[type='search'],.has-information[type='password'],.has-information[type='email'],.has-information[type='url'],.has-information[type='tel'],select.has-information,textarea.has-information{border:1px solid #24598f}[disabled][type='checkbox']+label,[disabled][type='radio']+label,[disabled='disabled'][type='checkbox']+label,[disabled='disabled'][type='radio']+label,[disabled][type='text'],[disabled][type='date'],[disabled][type='datetime'],[disabled][type='datetime-local'],[disabled][type='month'],[disabled][type='time'],[disabled][type='week'],[disabled][type='number'],[disabled][type='search'],[disabled][type='password'],[disabled][type='email'],[disabled][type='url'],[disabled][type='tel'],select[disabled],textarea[disabled],[disabled='disabled'][type='text'],[disabled='disabled'][type='date'],[disabled='disabled'][type='datetime'],[disabled='disabled'][type='datetime-local'],[disabled='disabled'][type='month'],[disabled='disabled'][type='time'],[disabled='disabled'][type='week'],[disabled='disabled'][type='number'],[disabled='disabled'][type='search'],[disabled='disabled'][type='password'],[disabled='disabled'][type='email'],[disabled='disabled'][type='url'],[disabled='disabled'][type='tel'],select[disabled='disabled'],textarea[disabled='disabled'],.p-checkbox__input:disabled+.p-checkbox__label,.p-radio__input:disabled+.p-radio__label,.p-switch:disabled+.p-switch__slider{cursor:not-allowed;opacity:.33}[readonly][type='text'],[readonly][type='date'],[readonly][type='datetime'],[readonly][type='datetime-local'],[readonly][type='month'],[readonly][type='time'],[readonly][type='week'],[readonly][type='number'],[readonly][type='search'],[readonly][type='password'],[readonly][type='email'],[readonly][type='url'],[readonly][type='tel'],select[readonly],textarea[readonly],[readonly='readonly'][type='text'],[readonly='readonly'][type='date'],[readonly='readonly'][type='datetime'],[readonly='readonly'][type='datetime-local'],[readonly='readonly'][type='month'],[readonly='readonly'][type='time'],[readonly='readonly'][type='week'],[readonly='readonly'][type='number'],[readonly='readonly'][type='search'],[readonly='readonly'][type='password'],[readonly='readonly'][type='email'],[readonly='readonly'][type='url'],[readonly='readonly'][type='tel'],select[readonly='readonly'],textarea[readonly='readonly']{color:#cdcdcd;cursor:default}:hover[readonly][type='text'],:hover[readonly][type='date'],:hover[readonly][type='datetime'],:hover[readonly][type='datetime-local'],:hover[readonly][type='month'],:hover[readonly][type='time'],:hover[readonly][type='week'],:hover[readonly][type='number'],:hover[readonly][type='search'],:hover[readonly][type='password'],:hover[readonly][type='email'],:hover[readonly][type='url'],:hover[readonly][type='tel'],select:hover[readonly],textarea:hover[readonly],:hover[readonly='readonly'][type='text'],:hover[readonly='readonly'][type='date'],:hover[readonly='readonly'][type='datetime'],:hover[readonly='readonly'][type='datetime-local'],:hover[readonly='readonly'][type='month'],:hover[readonly='readonly'][type='time'],:hover[readonly='readonly'][type='week'],:hover[readonly='readonly'][type='number'],:hover[readonly='readonly'][type='search'],:hover[readonly='readonly'][type='password'],:hover[readonly='readonly'][type='email'],:hover[readonly='readonly'][type='url'],:hover[readonly='readonly'][type='tel'],select:hover[readonly='readonly'],textarea:hover[readonly='readonly'],:active[readonly][type='text'],:active[readonly][type='date'],:active[readonly][type='datetime'],:active[readonly][type='datetime-local'],:active[readonly][type='month'],:active[readonly][type='time'],:active[readonly][type='week'],:active[readonly][type='number'],:active[readonly][type='search'],:active[readonly][type='password'],:active[readonly][type='email'],:active[readonly][type='url'],:active[readonly][type='tel'],select:active[readonly],textarea:active[readonly],:active[readonly='readonly'][type='text'],:active[readonly='readonly'][type='date'],:active[readonly='readonly'][type='datetime'],:active[readonly='readonly'][type='datetime-local'],:active[readonly='readonly'][type='month'],:active[readonly='readonly'][type='time'],:active[readonly='readonly'][type='week'],:active[readonly='readonly'][type='number'],:active[readonly='readonly'][type='search'],:active[readonly='readonly'][type='password'],:active[readonly='readonly'][type='email'],:active[readonly='readonly'][type='url'],:active[readonly='readonly'][type='tel'],select:active[readonly='readonly'],textarea:active[readonly='readonly']{border-color:#666;outline:none}label{max-width:40em;cursor:pointer;display:block;margin-bottom:.6rem;margin-top:0;padding-top:.4rem;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}label.is-required::after{color:#c7162b;content:'*';left:.25rem;position:relative}label.has-error{color:#c7162b}label.has-caution{color:#f99b11}label.has-success{color:#0e8620}label.has-information{color:#24598f}[type='file']{margin-bottom:1.2rem;width:100%}[type='file']:focus{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}[type='file']:focus-visible{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}[type='file']:focus:not(:focus-visible){outline:0;outline-offset:0}[type='search']{-moz-appearance:none;-webkit-appearance:none;appearance:none;border-radius:0}[type='search']::-webkit-search-results-decoration{display:none}[type='search']::-webkit-search-cancel-button{-webkit-appearance:searchfield-cancel-button;cursor:pointer}select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23666' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E");-moz-appearance:none;-webkit-appearance:none;appearance:none;background-color:#fff;background-position:right .5rem center;background-repeat:no-repeat;background-size:1rem;box-shadow:none;color:#111;min-height:1.5rem;padding-right:calc(1rem + 1rem);text-indent:0.01px;text-overflow:''}select:hover{cursor:pointer}select[multiple],select[size]{background-image:none;box-shadow:none;height:auto}select[multiple] option,select[size] option{font-weight:300;line-height:calc(1rem - 2px);padding:.5rem .5rem}textarea{margin-bottom:1.2rem;overflow:auto;vertical-align:top}fieldset{background-color:#f7f7f7;border:1px solid rgba(0,0,0,0.56);border-radius:.125rem;color:#111;margin-bottom:1.5rem;padding:calc(.5rem - 1px) .5rem}hr{margin-bottom:calc(.5rem - 1px)}hr.is-muted{background-color:rgba(0,0,0,0.1)}hr{background:rgba(0,0,0,0.2)}hr.is-dark{background:rgba(255,255,255,0.2)}a{color:#06c;text-decoration:none}a:focus{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}a:focus-visible{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}a:focus:not(:focus-visible){outline:0;outline-offset:0}a:focus{outline-offset:0}a:hover{cursor:pointer;text-decoration:underline}a:visited{color:#7d42b8}ol,ul{margin-bottom:1.5rem;margin-left:1rem;margin-top:0;padding-left:1rem}nav ol,nav ul{list-style:none;list-style-image:none}li{margin:0;padding:0}li>ul,li>ol{margin-bottom:0;padding-top:0}li>ul>li:last-of-type,li>ol>li:last-of-type{padding-bottom:0}dl{margin-bottom:1.5rem;margin-top:0;padding:0}dd{margin-left:1rem}img{border:0;border-radius:.125rem;height:auto;max-width:100%}svg:not(:root){overflow:hidden}figure{margin-bottom:1.5rem;margin-left:0;width:100%}figure caption,figure figcaption{display:block;font-style:italic;margin-top:.25rem;width:100%}iframe{border:0}object,iframe,embed,canvas,video,audio{display:block;margin:0 auto 20px;max-width:100%}audio:not([controls]){display:none;height:0}[hidden]{display:none}table{border:0;border-collapse:collapse;line-height:1.5rem;margin-bottom:1.5rem;overflow-x:auto;width:100%;table-layout:fixed}td,th{font-weight:300;overflow:hidden;padding-left:.5rem;padding-right:.5rem;text-align:left;text-overflow:ellipsis;vertical-align:top}thead th{line-height:1rem;padding-bottom:.75rem;padding-top:0.7505rem;text-transform:uppercase}thead tr{border-bottom:1px solid rgba(0,0,0,0.2);vertical-align:top}tfoot tr,tbody tr:not(:first-child){border-top:1px solid rgba(0,0,0,0.1)}.col-small-1,.col-small-2,.col-small-3,.col-small-4,.col-medium-1,.col-medium-2,.col-medium-3,.col-medium-4,.col-medium-5,.col-medium-6,.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12{display:block}@media (max-width: 620px){.col-medium-1,.col-medium-2,.col-medium-3,.col-medium-4,.col-medium-5,.col-medium-6,.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12{grid-column:auto/span 4}}@media (min-width: 620px) and (max-width: 772px){.col-small-1,.col-small-2,.col-small-3,.col-small-4,.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12{grid-column:auto/span 6}}@media (min-width: 772px){.col-small-1,.col-small-2,.col-small-3,.col-small-4,.col-medium-1,.col-medium-2,.col-medium-3,.col-medium-4,.col-medium-5,.col-medium-6{grid-column:auto/span 12}}hr.is-fixed-width,.p-divider,.row,.p-stepped-list--detailed .p-stepped-list__item,.p-navigation__row,.u-fixed-width{margin-left:auto;margin-right:auto;width:100%}hr.is-fixed-width,.p-divider,.row,.p-stepped-list--detailed .p-stepped-list__item,.p-navigation__row,.u-fixed-width{max-width:72rem}@media (min-width: 772px){.p-divider,.row,.p-stepped-list--detailed .p-stepped-list__item{display:flex}}.p-divider .p-divider,.row .p-divider,.p-stepped-list--detailed .p-stepped-list__item .p-divider,.p-divider .row,.row .row,.p-stepped-list--detailed .p-stepped-list__item .row,.p-divider .p-stepped-list--detailed .p-stepped-list__item,.p-stepped-list--detailed .p-divider .p-stepped-list__item,.row .p-stepped-list--detailed .p-stepped-list__item,.p-stepped-list--detailed .row .p-stepped-list__item,.p-stepped-list--detailed .p-stepped-list__item .p-stepped-list__item{margin-left:0;margin-right:0;padding-left:0;padding-right:0}@supports (display: grid){.p-divider,.row,.p-stepped-list--detailed .p-stepped-list__item{display:grid;grid-template-rows:auto;margin-left:auto;margin-right:auto;max-width:72rem}.p-divider [class*='col-'],.row [class*='col-'],.p-stepped-list--detailed .p-stepped-list__item [class*='col-']{grid-column-start:auto}@media (max-width: 620px){.p-divider,.row,.p-stepped-list--detailed .p-stepped-list__item{grid-gap:0 1.5rem;grid-template-columns:repeat(4, minmax(0, 1fr))}.p-divider>*,.row>*,.p-stepped-list--detailed .p-stepped-list__item>*{grid-column-end:span 4}}@media (min-width: 620px) and (max-width: 772px){.p-divider,.row,.p-stepped-list--detailed .p-stepped-list__item{grid-gap:0 2rem;grid-template-columns:repeat(6, minmax(0, 1fr))}.p-divider>*,.row>*,.p-stepped-list--detailed .p-stepped-list__item>*{grid-column-end:span 6}}@media (min-width: 772px){.p-divider,.row,.p-stepped-list--detailed .p-stepped-list__item{grid-gap:0 2rem;grid-template-columns:repeat(12, minmax(0, 1fr))}.p-divider>*,.row>*,.p-stepped-list--detailed .p-stepped-list__item>*{grid-column-end:span 12}}}@media (max-width: 620px){hr.is-fixed-width,.p-divider,.row,.p-stepped-list--detailed .p-stepped-list__item,.p-navigation__row,.u-fixed-width,.p-panel__header{padding-left:1rem;padding-right:1rem}}@media (min-width: 620px) and (max-width: 772px){hr.is-fixed-width,.p-divider,.row,.p-stepped-list--detailed .p-stepped-list__item,.p-navigation__row,.u-fixed-width,.p-panel__header{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width: 772px){hr.is-fixed-width,.p-divider,.row,.p-stepped-list--detailed .p-stepped-list__item,.p-navigation__row,.u-fixed-width,.p-panel__header{padding-left:1.5rem;padding-right:1.5rem}}.token.comment,.token.prolog,.token.doctype,.token.cdata{color:#666}.token.punctuation{color:#111}.token.namespace{opacity:0.7}.token.property,.token.tag,.token.boolean,.token.number,.token.constant,.token.symbol,.token.deleted{color:#77216f}.token.selector,.token.attr-name,.token.string,.token.char,.token.builtin,.token.inserted{color:#0e811f}.token .operator,.token .entity,.token .url,.language-css .token.string,.style .token.string{color:#a86500}.token.atrule,.token.attr-value,.token.keyword{color:#06c}.token.function,.token.class-name{color:#c7162b}.token.regex,.token.important,.token.variable{color:#dc3023}.token.important,.token.bold{font-weight:bold}.token.italic{font-style:italic}.token.entity{cursor:help}.p-accordion__list{list-style-type:none;margin:0 0 1.5rem 0;padding:0}.p-accordion__tab,.p-accordion__tab--with-title{background-color:inherit;border:0;border-radius:0;justify-content:flex-start;margin-bottom:0;padding-left:3rem;padding-right:1rem;text-align:left;transition-duration:0s;width:100%;z-index:2}.p-accordion__tab:focus,.p-accordion__tab--with-title:focus{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-accordion__tab:focus-visible,.p-accordion__tab--with-title:focus-visible{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-accordion__tab:focus:not(:focus-visible),.p-accordion__tab--with-title:focus:not(:focus-visible){outline:0;outline-offset:0}.p-accordion__tab{background-position:top calc(.5rem - 1px + 0.25rem) left 1rem;background-repeat:no-repeat}.p-accordion__tab[aria-expanded='true']{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23666' fill-rule='nonzero' d='M14.849 7.25v1.5H1.15v-1.5z'/%3E%3C/svg%3E");background-size:1rem}.p-accordion__tab[aria-expanded='false']{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23666' fill-rule='nonzero' d='M8.75 1.151V7.25l6.099.001v1.5h-6.1l.001 6.099h-1.5v-6.1L1.15 8.75v-1.5H7.25L7.25 1.15z'/%3E%3C/svg%3E");background-size:1rem}.p-accordion__title{margin-bottom:0;padding-top:0;position:relative;text-indent:-2rem}.p-accordion__title::before{background-size:1rem;content:'';display:inline-block;height:1rem;margin-right:1rem;width:1rem}h2.p-accordion__title::before{background-size:.517em;height:.517em;width:.517em}h5.p-accordion__title::before,h6.p-accordion__title::before{vertical-align:calc(.5px + 0.3465em - 0.5rem)}.p-accordion__tab--with-title[aria-expanded='true'] .p-accordion__title::before{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23666' fill-rule='nonzero' d='M14.849 7.25v1.5H1.15v-1.5z'/%3E%3C/svg%3E")}.p-accordion__tab--with-title[aria-expanded='false'] .p-accordion__title::before{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23666' fill-rule='nonzero' d='M8.75 1.151V7.25l6.099.001v1.5h-6.1l.001 6.099h-1.5v-6.1L1.15 8.75v-1.5H7.25L7.25 1.15z'/%3E%3C/svg%3E")}.p-accordion__panel{margin:0;overflow:auto;padding-left:4rem}.p-accordion__panel[aria-hidden='true']{display:none}.p-article-pagination__link--previous::before,.p-article-pagination__link--next::after{color:#666;content:'\203A';font-size:2em;position:absolute;top:1rem}.p-article-pagination{display:flex;width:100%}.p-article-pagination__label,.p-article-pagination__title{color:#111;display:block;margin-top:0;width:100%}.p-article-pagination__label{margin-bottom:.25rem}.p-article-pagination__title{font-size:1.125em}@media (min-width: 620px){.p-article-pagination__title{font-size:1.25em}}.p-article-pagination__link,.p-article-pagination__link--previous,.p-article-pagination__link--next{margin-top:0;padding:1rem;position:relative;width:50%}.p-article-pagination__link:hover,.p-article-pagination__link--previous:hover,.p-article-pagination__link--next:hover{background:#f7f7f7;text-decoration:none}.p-article-pagination__link--previous{padding-left:2.5rem;text-align:left}@media (max-width: 460px){.p-article-pagination__link--previous{width:auto}.p-article-pagination__link--previous:only-child{width:100%}.p-article-pagination__link--previous:not(:only-child) *{display:none;max-width:.25rem;padding-left:1.5rem}}.p-article-pagination__link--previous::before{left:.5rem;transform:scaleX(-1)}.p-article-pagination__link--next{padding-right:2.5rem;text-align:right}@media (max-width: 460px){.p-article-pagination__link--next{width:100%}}.p-article-pagination__link--next:only-child{margin-left:auto}.p-article-pagination__link--next::after{right:.5rem}.p-breadcrumbs{list-style:none;margin:0;padding:0;width:100%}.p-breadcrumbs__items{margin-left:0;padding-left:0}.p-breadcrumbs__item{display:inline-block;margin-bottom:.6rem}.p-breadcrumbs__item:not(:first-of-type){text-indent:1rem}.p-breadcrumbs__item:not(:first-of-type)::before{content:'\203A';margin-left:-0.75rem;margin-right:0.5rem}.p-button{background-color:#fff;border-color:rgba(0,0,0,0.56);color:#111}.p-button:visited{color:#111}.p-button:hover{background-color:#f2f2f2;border-color:rgba(0,0,0,0.56)}.p-button:active{background-color:#d9d9d9;border-color:#fff;transition-duration:0s}.p-button:disabled:active,.p-button:disabled:hover,.is-disabled.p-button:active,.is-disabled.p-button:hover{background-color:#fff;border-color:#fff}.p-button .p-link--external{color:currentColor}.p-button--neutral{background-color:#fff;border-color:rgba(0,0,0,0.56);color:#111}.p-button--neutral:visited{color:#111}.p-button--neutral:hover{background-color:#f2f2f2;border-color:rgba(0,0,0,0.56)}.p-button--neutral:active{background-color:#d9d9d9;border-color:rgba(0,0,0,0.56);transition-duration:0s}.p-button--neutral:disabled:active,.p-button--neutral:disabled:hover,.is-disabled.p-button--neutral:active,.is-disabled.p-button--neutral:hover{background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0.56)}.p-button--neutral .p-link--external{color:currentColor}.p-button--brand{background-color:#333;border-color:#333;color:#fff}.p-button--brand:visited{color:#fff}.p-button--brand:hover{background-color:#303030;border-color:rgba(0,0,0,0.56)}.p-button--brand:active{background-color:#2b2b2b;border-color:#2b2b2b;transition-duration:0s}.p-button--brand:disabled:active,.p-button--brand:disabled:hover,.is-disabled.p-button--brand:active,.is-disabled.p-button--brand:hover{background-color:#333;border-color:#333}.p-button--brand .p-link--external{color:currentColor}.p-button--positive{background-color:#0e8620;border-color:#0e8620;color:#fff}.p-button--positive:visited{color:#fff}.p-button--positive:hover{background-color:#0d7f1e;border-color:rgba(0,0,0,0.56)}.p-button--positive:active{background-color:#0c721b;border-color:rgba(0,0,0,0.56);transition-duration:0s}.p-button--positive:disabled:active,.p-button--positive:disabled:hover,.is-disabled.p-button--positive:active,.is-disabled.p-button--positive:hover{background-color:#0e8620;border-color:#0e8620}.p-button--positive .p-link--external{color:currentColor}.p-button--positive:focus{outline:.1875rem solid #003008;outline-offset:-.1875rem}.p-button--positive:focus-visible{outline:.1875rem solid #003008;outline-offset:-.1875rem}.p-button--positive:focus:not(:focus-visible){outline:0;outline-offset:0}.p-button--negative{background-color:#c7162b;border-color:#c7162b;color:#fff}.p-button--negative:visited{color:#fff}.p-button--negative:hover{background-color:#bd1529;border-color:rgba(0,0,0,0.56)}.p-button--negative:active{background-color:#a91325;border-color:#a91325;transition-duration:0s}.p-button--negative:disabled:active,.p-button--negative:disabled:hover,.is-disabled.p-button--negative:active,.is-disabled.p-button--negative:hover{background-color:#c7162b;border-color:#c7162b}.p-button--negative .p-link--external{color:currentColor}.p-button--negative:focus{outline:.1875rem solid #3b0006;outline-offset:-.1875rem}.p-button--negative:focus-visible{outline:.1875rem solid #3b0006;outline-offset:-.1875rem}.p-button--negative:focus:not(:focus-visible){outline:0;outline-offset:0}.p-button--base{background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0);color:#111}.p-button--base:visited{color:#111}.p-button--base:hover{background-color:#f2f2f2;border-color:rgba(0,0,0,0)}.p-button--base:active{background-color:#d9d9d9;border-color:rgba(0,0,0,0);transition-duration:0s}.p-button--base:disabled:active,.p-button--base:disabled:hover,.is-disabled.p-button--base:active,.is-disabled.p-button--base:hover{background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0.56)}.p-button--base .p-link--external{color:currentColor}@media (min-width: 460px){[class*='p-button'].is-inline{margin-left:1rem;width:auto}}[class*='p-button'].is-processing{opacity:1 !important}[class*='p-button'].is-active{opacity:1 !important}[class*='p-button'].has-icon{width:auto}[class*='p-button'].has-icon [class*='p-icon']{margin-left:.5rem;margin-right:.5rem}[class*='p-button'].has-icon [class*='p-icon']:first-child{margin-left:-.5rem}[class*='p-button'].has-icon [class*='p-icon']:last-child{margin-right:-.5rem}.p-card{padding:calc(1rem - 1px)}.p-card--overlay{background:rgba(255,255,255,0.9);color:#111;margin-bottom:1.5rem;overflow:auto;padding:1rem}.p-card--muted{margin-bottom:1.5rem;overflow:auto;padding:1rem}.p-card__image{margin-bottom:1rem;vertical-align:top;width:100%}.p-card__header{border-bottom:1px solid #cdcdcd;padding-bottom:1rem}.p-card__header>.p-link--soft{display:inline-block;overflow:auto}.p-card__thumbnail{max-height:2rem}[class*='p-card']>p:not([class*='p-heading--']):last-child,[class*='p-card']>h5:last-child,[class*='p-card']>h6:last-child{margin-bottom:.1rem}[class*='p-card']>p:not([class*='p-heading--']):first-child,[class*='p-card']>h5:first-child,[class*='p-card']>h6:first-child{margin-top:-.5rem}.p-chip{background-color:rgba(0,0,0,0.05);border-radius:1rem;display:inline-flex;line-height:1rem;margin:.4rem .5rem 0 0;max-width:100%;padding-bottom:0.25rem;padding-left:1rem;padding-right:1rem;padding-top:0.25rem;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;white-space:nowrap}.p-chip:hover{background-color:rgba(0,0,0,0.08)}.p-chip.is-selected{background-color:rgba(0,0,0,0.15)}.p-chip.is-selected:hover{background-color:rgba(0,0,0,0.18)}.p-chip:active{background-color:rgba(0,0,0,0.15)}.p-chip__lead,.p-chip__value{margin-bottom:0;overflow:hidden;text-overflow:ellipsis}.p-chip__lead{line-height:1rem;padding-right:.5rem;padding-top:0.1rem;position:relative;text-transform:uppercase}.p-chip__lead::after{content:'\00a0:';position:absolute;right:.25rem}.p-chip__value{line-height:1rem;padding-top:0.05rem}.p-chip__dismiss{background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0);color:#111;border:none;display:block;flex:0 0 auto;left:.25rem;line-height:1rem;margin-bottom:0;margin-right:-.25rem;padding:0;position:relative;top:0.05rem}.p-chip__dismiss:visited{color:#111}.p-chip__dismiss:hover{background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0)}.p-chip__dismiss:active{background-color:#d9d9d9;border-color:rgba(0,0,0,0.56);transition-duration:0s}.p-chip__dismiss:disabled:active,.p-chip__dismiss:disabled:hover,.p-chip__dismiss.is-disabled:active,.p-chip__dismiss.is-disabled:hover{background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0.56)}.p-chip__dismiss .p-link--external{color:currentColor}@media only screen and (max-width: 460px){.p-chip__dismiss{width:auto}}.p-chip__dismiss [class*='p-icon']{vertical-align:calc(0.3465em - 0.5rem)}.p-code-copyable{background-color:rgba(0,0,0,0.03);margin-bottom:1.2rem;overflow:hidden;padding:.5rem 1rem;position:relative}.p-code-copyable+.p-code-copyable{margin-top:0}.p-code-copyable__input{background:transparent;border:0;font-family:"Ubuntu Mono", Consolas, Monaco, Courier, monospace;font-weight:300;line-height:1.5rem;margin-bottom:0;margin-top:0;outline:none;padding:0 0 0 1.5rem;width:100%}.p-code-copyable__action{display:none}.p-code-numbered{counter-reset:line-numbering}.p-code-snippet{margin-bottom:1.5rem}.p-code-snippet .p-code-snippet__block,.p-code-snippet .p-code-snippet__block--icon,.p-code-snippet .p-code-snippet__block--numbered{margin:0}.p-code-snippet .p-code-snippet__block.is-wrapped,.p-code-snippet .p-code-snippet__block--icon.is-wrapped,.p-code-snippet .p-code-snippet__block--numbered.is-wrapped{white-space:pre-wrap}.p-code-snippet .p-code-snippet__block--icon{padding-left:2.5rem;position:relative}.p-code-snippet .p-code-snippet__block--icon.is-windows-prompt::before{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23666' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E");transform:rotate(270deg)}.p-code-snippet .p-code-snippet__block--icon.is-url::before{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8.781 4.772c.976.683 1.5 1.77 1.508 2.876l-.065.105c-.317.457-.88.637-1.308.399a.831.831 0 01-.162-.117A2.036 2.036 0 005.177 6.38l-.091.121L2.832 9.72a2.036 2.036 0 003.243 2.456l.092-.121.114-.162a3.582 3.582 0 001.34.7l-.225.322a3.536 3.536 0 11-5.792-4.056L3.857 5.64a3.536 3.536 0 014.924-.868zm4.747-3.042a3.536 3.536 0 01.868 4.924l-2.253 3.218A3.536 3.536 0 015.71 7.865l.065-.105c.317-.458.88-.637 1.308-.4.06.033.113.073.162.117a2.036 2.036 0 003.577 1.656l.091-.121 2.254-3.218a2.036 2.036 0 00-3.243-2.457l-.092.122-.114.162a3.582 3.582 0 00-1.34-.701l.225-.322a3.536 3.536 0 014.924-.868z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-code-snippet .p-code-snippet__block--numbered{counter-reset:line-numbering}.p-code-snippet .p-code-snippet__header{align-items:flex-start;background-color:rgba(0,0,0,0.08);display:flex;justify-content:space-between;padding-left:1rem;padding-right:1rem}.p-code-snippet .p-code-snippet__title{flex:0 1 auto;margin-bottom:.5rem;overflow:hidden;padding-top:.5rem;text-overflow:ellipsis;white-space:nowrap}.p-code-snippet .p-code-snippet__dropdown{background-color:rgba(0,0,0,0);border:0;color:#111;margin-bottom:0;margin-left:.5rem;min-width:-webkit-min-content;min-width:-moz-min-content;min-width:min-content;padding-bottom:.5rem;padding-left:1rem;padding-top:.5rem;width:-webkit-min-content;width:-moz-min-content;width:min-content}.p-code-snippet .p-code-snippet__dropdowns{display:flex;flex-shrink:0}.p-code-snippet .p-code-snippet__dropdowns:first-child{margin-left:auto}.p-code-snippet .p-code-snippet__dropdown+.p-code-snippet__dropdown{border-left:1px solid rgba(0,0,0,0.2)}.p-code-snippet .p-code-snippet__header.is-stacked{flex-direction:column}.p-code-snippet .p-code-snippet__header.is-stacked .p-code-snippet__title{white-space:normal}.p-code-snippet .p-code-snippet__header.is-stacked .p-code-snippet__dropdowns{flex:1 0 100%;justify-content:flex-end;width:100%}.p-code-snippet .p-code-snippet__header.is-stacked .p-code-snippet__title+.p-code-snippet__dropdowns{border-top:1px solid rgba(0,0,0,0.2)}.p-contextual-menu,.p-contextual-menu--left,.p-contextual-menu--center{display:inline-block;margin:0;position:relative}.p-contextual-menu__dropdown{display:none;margin:0;max-width:21rem;min-width:10rem;padding:0;position:absolute;right:0;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;z-index:9}.p-contextual-menu__dropdown[aria-hidden='false']{display:block}.p-contextual-menu--left .p-contextual-menu__dropdown{left:0}.p-contextual-menu--center .p-contextual-menu__dropdown{left:50%;transform:translateX(-50%)}.p-contextual-menu__group{display:block}.p-contextual-menu__group+.p-contextual-menu__group{border-top-style:solid;border-top-width:1px;margin:-1px 0 0 0}.p-contextual-menu__link{border:0;clear:both;display:block;margin:0;overflow:hidden;padding:.25rem 1rem;text-align:left;text-overflow:ellipsis;white-space:nowrap;width:100%}.p-contextual-menu__link:focus{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-contextual-menu__link:focus-visible{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-contextual-menu__link:focus:not(:focus-visible){outline:0;outline-offset:0}.p-contextual-menu__link:hover{border-radius:.125rem;text-decoration:none}.p-contextual-menu__toggle[aria-expanded='true'] .p-contextual-menu__indicator{transform:rotate(180deg)}.p-contextual-menu__dropdown{background:#fff}.p-contextual-menu__group+.p-contextual-menu__group{border-top-color:rgba(0,0,0,0.2)}.p-contextual-menu__link,.p-contextual-menu__link:active,.p-contextual-menu__link:hover,.p-contextual-menu__link:visited{color:#111}.p-contextual-menu__link:hover{background-color:rgba(0,0,0,0.05)}.p-contextual-menu__link:active{background-color:rgba(0,0,0,0.15)}[class*='p-contextual-menu'].is-dark .p-contextual-menu__dropdown{background:#262626}[class*='p-contextual-menu'].is-dark .p-contextual-menu__group+.p-contextual-menu__group{border-top-color:rgba(255,255,255,0.2)}[class*='p-contextual-menu'].is-dark .p-contextual-menu__link,[class*='p-contextual-menu'].is-dark .p-contextual-menu__link:active,[class*='p-contextual-menu'].is-dark .p-contextual-menu__link:hover,[class*='p-contextual-menu'].is-dark .p-contextual-menu__link:visited{color:#fff}[class*='p-contextual-menu'].is-dark .p-contextual-menu__link:hover{background-color:rgba(255,255,255,0.05)}[class*='p-contextual-menu'].is-dark .p-contextual-menu__link:active{background-color:rgba(255,255,255,0.15)}.p-divider__block{position:relative}@media (max-width: 772px){.p-divider__block{padding-bottom:1.5rem;padding-top:1rem}.p-divider__block:not(:first-child)::before{background-color:#cdcdcd;content:'';height:1px;left:0;position:absolute;right:0;top:0}}@media (min-width: 772px){.p-divider__block:not(:first-child)::before{background-color:#cdcdcd;bottom:0;content:'';left:-1rem;position:absolute;top:0;width:1px}}.p-form-help-text{color:#666;margin-top:-.5rem}.p-form-validation{color:#111;position:relative}.p-form-validation :not(select).p-form-validation__input{background-position:calc(100% - .5rem) 50%;background-repeat:no-repeat}.p-form-validation__message{margin-top:-.5rem}.p-form-validation__icon{position:relative}.p-form-validation__icon::after{position:absolute;right:.5rem;top:calc(50% - .25rem)}.is-success .p-form-validation__input,.is-error .p-form-validation__input,.is-caution .p-form-validation__input{padding-right:2rem}.is-error .p-form-validation__select-wrapper,.is-caution .p-form-validation__select-wrapper,.is-success .p-form-validation__select-wrapper{min-width:10em;position:relative}.is-error .p-form-validation__select-wrapper::after,.is-caution .p-form-validation__select-wrapper::after,.is-success .p-form-validation__select-wrapper::after{background-repeat:no-repeat;background-size:contain;content:' ';display:block;height:1rem;pointer-events:none;position:absolute;right:2rem;top:calc(50% - (1rem / 2) - (1.2rem / 2));width:1rem}.is-error .p-form-validation__select-wrapper .p-form-validation__input,.is-caution .p-form-validation__select-wrapper .p-form-validation__input,.is-success .p-form-validation__select-wrapper .p-form-validation__input{padding-right:3rem}.is-success .p-form-validation__input{border-color:#0e8620}.is-success :not(select).p-form-validation__input,.is-success .p-form-validation__select-wrapper::after{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cg fill='none' fill-rule='nonzero'%3E%3Cpath fill='%230e8620' d='M8 1a7 7 0 110 14A7 7 0 018 1zm2.83 3.502L6.863 9.884 5.174 8.096l-1.09 1.03 2.92 3.096 5.034-6.83-1.208-.89z'/%3E%3Cpath fill='%23fff' d='M10.83 4.502l1.208.89-5.033 6.83-2.922-3.096 1.091-1.03 1.689 1.789z'/%3E%3C/g%3E%3C/svg%3E")}.is-caution .p-form-validation__input{border-color:#f99b11}.is-caution :not(select).p-form-validation__input,.is-caution .p-form-validation__select-wrapper::after{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M9.34 1.2l5.842 11.627A1.5 1.5 0 0113.842 15H2.158a1.5 1.5 0 01-1.34-2.173L6.66 1.2a1.5 1.5 0 012.68 0z' fill='%23f99b11'/%3E%3Cpath d='M8.5 11a.5.5 0 01.492.41L9 11.5v1a.5.5 0 01-.41.492L8.5 13h-1a.5.5 0 01-.492-.41L7 12.5v-1a.5.5 0 01.41-.492L7.5 11h1zM9 5v4.5H7V5h2z' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E")}.is-error .p-form-validation__input{border-color:#c7162b}.is-error :not(select).p-form-validation__input,.is-error .p-form-validation__select-wrapper::after{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle stroke='%23C7162B' stroke-width='1.5' fill='%23c7162b' cx='8' cy='8' r='6.25'/%3E%3Cpath fill='%23FFF' fill-rule='nonzero' d='M10.282 4.638l1.06 1.06L9.05 7.99l2.293 2.292-1.06 1.06L7.99 9.05 5.7 11.343l-1.06-1.06 2.29-2.293L4.64 5.7l1.06-1.06 2.291 2.29z'/%3E%3C/g%3E%3C/svg%3E")}.p-checkbox,.p-checkbox--heading,.p-checkbox--inline,.p-radio,.p-radio--heading,.p-radio--inline{position:relative}[type='checkbox'].p-checkbox__input,[type='radio'].p-radio__input{bottom:0.2em}.p-checkbox--heading,.p-checkbox--inline,.p-radio--heading,.p-radio--inline{display:inline}.p-checkbox--heading .p-checkbox__label::before,.p-checkbox--heading .p-checkbox__label::after,.p-radio--heading .p-radio__label::before,.p-radio--heading .p-radio__label::after{margin-top:-.15rem}.p-radio+.p-radio,.p-checkbox+.p-checkbox,.p-checkbox+.p-radio,.p-radio+.p-checkbox{margin-top:-.5rem}.p-checkbox__label{color:#111}.p-checkbox__label::before{background:#fff;border:1px solid rgba(0,0,0,0.56)}.p-checkbox__label::after{color:#fff}.p-radio__label{color:#111}.p-radio__label::before{background:#fff;border:1px solid rgba(0,0,0,0.56)}.p-radio__label::after{background-color:#fff}.p-checkbox.is-dark .p-checkbox__label{color:#fff}.p-checkbox.is-dark .p-checkbox__label::before{background:#262626;border:1px solid rgba(255,255,255,0.4)}.p-checkbox.is-dark .p-checkbox__label::after{color:#fff}.p-radio.is-dark .p-radio__label{color:#fff}.p-radio.is-dark .p-radio__label::before{background:#262626;border:1px solid rgba(255,255,255,0.4)}.p-radio.is-dark .p-radio__label::after{background-color:#fff}.p-form--stacked{width:100%}@media screen and (min-width: 772px){.p-form--stacked .p-form__group{align-items:baseline}.p-form--stacked .p-form__group+.p-form__group{margin-top:.5rem}}@media screen and (min-width: 772px){.p-form--inline{align-items:baseline;display:inline-flex;flex-direction:row;flex-wrap:wrap}.p-form--inline>*{margin-right:1.5rem}}@media screen and (min-width: 772px){.p-form--inline .p-form__group{display:flex;flex-shrink:1;width:auto}.p-form--inline .p-form__group+[class*='p-button']{flex-shrink:1}.p-form--inline .p-form__group .p-form__label,.p-form--inline .p-form__group .p-form__control,.p-form--inline .p-form__group .p-form-validation__message{align-self:baseline;box-sizing:border-box}.p-form--inline .p-form__group .p-form__label{flex-shrink:0;padding-right:1rem}.p-form--inline .p-form__group .p-form__control{display:inline-block}}form+[class*='p-button']{margin-top:1.5rem}.row{width:100%}.grid-demo [class*='col-']{background:rgba(199,22,43,0.1);margin-bottom:.5rem}@media (max-width: 620px){.col-small-4{width:100%}@supports (display: grid){.col-small-4{grid-column-end:span 4;margin-left:0;width:auto}.col-small-4 .row{grid-template-columns:repeat(4, minmax(0, 1fr))}}.col-small-3{width:100%}@supports (display: grid){.col-small-3{grid-column-end:span 3;margin-left:0;width:auto}.col-small-3 .row{grid-template-columns:repeat(3, minmax(0, 1fr))}}.col-small-2{width:100%}@supports (display: grid){.col-small-2{grid-column-end:span 2;margin-left:0;width:auto}.col-small-2 .row{grid-template-columns:repeat(2, minmax(0, 1fr))}}.col-small-1{width:100%}@supports (display: grid){.col-small-1{grid-column-end:span 1;margin-left:0;width:auto}}}@media (min-width: 620px) and (max-width: 772px){.col-medium-6{width:100%}@supports (display: grid){.col-medium-6{grid-column-end:span 6;margin-left:0;width:auto}.col-medium-6 .row{grid-template-columns:repeat(6, minmax(0, 1fr))}}.col-medium-5{width:100%}@supports (display: grid){.col-medium-5{grid-column-end:span 5;margin-left:0;width:auto}.col-medium-5 .row{grid-template-columns:repeat(5, minmax(0, 1fr))}}.col-medium-4{width:100%}@supports (display: grid){.col-medium-4{grid-column-end:span 4;margin-left:0;width:auto}.col-medium-4 .row{grid-template-columns:repeat(4, minmax(0, 1fr))}}.col-medium-3{width:100%}@supports (display: grid){.col-medium-3{grid-column-end:span 3;margin-left:0;width:auto}.col-medium-3 .row{grid-template-columns:repeat(3, minmax(0, 1fr))}}.col-medium-2{width:100%}@supports (display: grid){.col-medium-2{grid-column-end:span 2;margin-left:0;width:auto}.col-medium-2 .row{grid-template-columns:repeat(2, minmax(0, 1fr))}}.col-medium-1{width:100%}@supports (display: grid){.col-medium-1{grid-column-end:span 1;margin-left:0;width:auto}}}@media (min-width: 772px){.col-12{flex-basis:0;flex-grow:12;flex-shrink:1;margin-left:2rem}.col-12:first-child{margin-left:0}@supports (display: grid){.col-12{grid-column-end:span 12;margin-left:0;width:auto}.col-12 .row{grid-template-columns:repeat(12, minmax(0, 1fr))}}.col-11{flex-basis:0;flex-grow:11;flex-shrink:1;margin-left:2rem}.col-11:first-child{margin-left:0}@supports (display: grid){.col-11{grid-column-end:span 11;margin-left:0;width:auto}.col-11 .row{grid-template-columns:repeat(11, minmax(0, 1fr))}}.col-10{flex-basis:0;flex-grow:10;flex-shrink:1;margin-left:2rem}.col-10:first-child{margin-left:0}@supports (display: grid){.col-10{grid-column-end:span 10;margin-left:0;width:auto}.col-10 .row{grid-template-columns:repeat(10, minmax(0, 1fr))}}.col-9{flex-basis:0;flex-grow:9;flex-shrink:1;margin-left:2rem}.col-9:first-child{margin-left:0}@supports (display: grid){.col-9{grid-column-end:span 9;margin-left:0;width:auto}.col-9 .row{grid-template-columns:repeat(9, minmax(0, 1fr))}}.col-8{flex-basis:0;flex-grow:8;flex-shrink:1;margin-left:2rem}.col-8:first-child{margin-left:0}@supports (display: grid){.col-8{grid-column-end:span 8;margin-left:0;width:auto}.col-8 .row{grid-template-columns:repeat(8, minmax(0, 1fr))}}.col-7{flex-basis:0;flex-grow:7;flex-shrink:1;margin-left:2rem}.col-7:first-child{margin-left:0}@supports (display: grid){.col-7{grid-column-end:span 7;margin-left:0;width:auto}.col-7 .row{grid-template-columns:repeat(7, minmax(0, 1fr))}}.col-6{flex-basis:0;flex-grow:6;flex-shrink:1;margin-left:2rem}.col-6:first-child{margin-left:0}@supports (display: grid){.col-6{grid-column-end:span 6;margin-left:0;width:auto}.col-6 .row{grid-template-columns:repeat(6, minmax(0, 1fr))}}.col-5{flex-basis:0;flex-grow:5;flex-shrink:1;margin-left:2rem}.col-5:first-child{margin-left:0}@supports (display: grid){.col-5{grid-column-end:span 5;margin-left:0;width:auto}.col-5 .row{grid-template-columns:repeat(5, minmax(0, 1fr))}}.col-4{flex-basis:0;flex-grow:4;flex-shrink:1;margin-left:2rem}.col-4:first-child{margin-left:0}@supports (display: grid){.col-4{grid-column-end:span 4;margin-left:0;width:auto}.col-4 .row{grid-template-columns:repeat(4, minmax(0, 1fr))}}.col-3{flex-basis:0;flex-grow:3;flex-shrink:1;margin-left:2rem}.col-3:first-child{margin-left:0}@supports (display: grid){.col-3{grid-column-end:span 3;margin-left:0;width:auto}.col-3 .row{grid-template-columns:repeat(3, minmax(0, 1fr))}}.col-2{flex-basis:0;flex-grow:2;flex-shrink:1;margin-left:2rem}.col-2:first-child{margin-left:0}@supports (display: grid){.col-2{grid-column-end:span 2;margin-left:0;width:auto}.col-2 .row{grid-template-columns:repeat(2, minmax(0, 1fr))}}.col-1{flex-basis:0;flex-grow:1;flex-shrink:1;margin-left:2rem}.col-1:first-child{margin-left:0}@supports (display: grid){.col-1{grid-column-end:span 1;margin-left:0;width:auto}}}@media (max-width: 620px){.row [class*='col-'].col-start-small-1{grid-column-start:1}.col-order-small-1{order:1}.row [class*='col-'].col-start-small-2{grid-column-start:2}.col-order-small-2{order:2}.row [class*='col-'].col-start-small-3{grid-column-start:3}.col-order-small-3{order:3}}@media (min-width: 620px) and (max-width: 772px){.row [class*='col-'].col-start-medium-1{grid-column-start:1}.col-order-medium-1{order:1}.row [class*='col-'].col-start-medium-2{grid-column-start:2}.col-order-medium-2{order:2}.row [class*='col-'].col-start-medium-3{grid-column-start:3}.col-order-medium-3{order:3}.row [class*='col-'].col-start-medium-4{grid-column-start:4}.col-order-medium-4{order:4}.row [class*='col-'].col-start-medium-5{grid-column-start:5}.col-order-medium-5{order:5}}@media (min-width: 772px){.row [class*='col-'].col-start-large-1{grid-column-start:1}.col-order-large-1{order:1}.row [class*='col-'].col-start-large-2{grid-column-start:2}.col-order-large-2{order:2}.row [class*='col-'].col-start-large-3{grid-column-start:3}.col-order-large-3{order:3}.row [class*='col-'].col-start-large-4{grid-column-start:4}.col-order-large-4{order:4}.row [class*='col-'].col-start-large-5{grid-column-start:5}.col-order-large-5{order:5}.row [class*='col-'].col-start-large-6{grid-column-start:6}.col-order-large-6{order:6}.row [class*='col-'].col-start-large-7{grid-column-start:7}.col-order-large-7{order:7}.row [class*='col-'].col-start-large-8{grid-column-start:8}.col-order-large-8{order:8}.row [class*='col-'].col-start-large-9{grid-column-start:9}.col-order-large-9{order:9}.row [class*='col-'].col-start-large-10{grid-column-start:10}.col-order-large-10{order:10}.row [class*='col-'].col-start-large-11{grid-column-start:11}.col-order-large-11{order:11}}.row.is-bordered{position:relative}.row.is-bordered::before{background:#cdcdcd;content:'';height:1px;margin-bottom:calc(1rem - 1px);position:absolute}@media (max-width: 620px){.row.is-bordered::before{left:1rem;right:1rem}}@media (min-width: 620px) and (max-width: 772px){.row.is-bordered::before{left:1.5rem;right:1.5rem}}@media (min-width: 772px){.row.is-bordered::before{left:1.5rem;right:1.5rem}}@media (min-width: 1036px){.l-fluid-breakout{display:flex}}.l-fluid-breakout .l-fluid-breakout__item{margin-right:1rem}@media (min-width: 86rem){.l-fluid-breakout .l-fluid-breakout__item{flex:1 1 auto;width:13rem}}.l-fluid-breakout .l-fluid-breakout__main{display:flex;flex-wrap:wrap;width:100%}@media (min-width: 86rem){.l-fluid-breakout .l-fluid-breakout__main{width:calc(100% - 14rem)}}.l-fluid-breakout .l-fluid-breakout__aside--right,.l-fluid-breakout .l-fluid-breakout__aside{flex:0 0 auto;justify-content:flex-start}@media (min-width: 1036px){.l-fluid-breakout .l-fluid-breakout__aside--right,.l-fluid-breakout .l-fluid-breakout__aside{flex:1 1 auto;width:14rem}}@media (max-width: 86rem){.l-fluid-breakout .l-fluid-breakout__aside{padding-left:0}}@supports (display: grid){.l-fluid-breakout{display:block;grid-gap:0 0}}@media (min-width: 1036px){.l-fluid-breakout{margin-left:auto;margin-right:auto;max-width:100rem}@supports (display: grid){.l-fluid-breakout{display:grid;grid-template-columns:minmax(14rem, 1fr) minmax(0, 72rem) minmax(14rem, 1fr);grid-template-rows:auto}}}@media (max-width: 620px){.l-fluid-breakout{padding-left:1rem;padding-right:1rem}}@media (min-width: 620px) and (max-width: 772px){.l-fluid-breakout{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width: 772px) and (max-width: 86rem){.l-fluid-breakout{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width: 86rem){.l-fluid-breakout{padding-left:0;padding-right:0}@supports (display: grid){.l-fluid-breakout{grid-gap:0 0}}}@supports (display: grid){.l-fluid-breakout .l-fluid-breakout__main{display:grid;grid-gap:0 1rem;grid-row:1;grid-template-columns:repeat(auto-fit, minmax(13rem, 1fr));width:100%}}@media (min-width: 86rem){.l-fluid-breakout .l-fluid-breakout__main{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width: 1036px){@supports (display: grid){.l-fluid-breakout .l-fluid-breakout__main{grid-column:auto / span 2}.l-fluid-breakout .l-fluid-breakout__main.no-aside{grid-column:2 / span 2}}}@media (min-width: 1036px){@supports (display: grid){.l-fluid-breakout .l-fluid-breakout__main.is-full-width{grid-column:1 / span 3}}}.l-fluid-breakout .l-fluid-breakout__main .row{max-width:100%;padding-left:0;padding-right:0}@supports (display: grid){.l-fluid-breakout .l-fluid-breakout__item{margin-left:0;margin-right:0}@media (min-width: 86rem){.l-fluid-breakout .l-fluid-breakout__item{grid-column:auto / auto;width:initial}}}.l-fluid-breakout .l-fluid-breakout__aside,.l-fluid-breakout .l-fluid-breakout__aside--right{padding-left:0}@media (min-width: 86rem){.l-fluid-breakout .l-fluid-breakout__aside,.l-fluid-breakout .l-fluid-breakout__aside--right{align-items:start}}@supports (display: grid){.l-fluid-breakout .l-fluid-breakout__aside,.l-fluid-breakout .l-fluid-breakout__aside--right{grid-column-end:span 1;grid-column-start:auto;grid-row:1 / 100}}@media (min-width: 1036px) and (max-width: 86rem){.l-fluid-breakout .l-fluid-breakout__aside{padding-right:1.5rem}}@media (min-width: 86rem){.l-fluid-breakout .l-fluid-breakout__aside{padding-left:1.5rem;padding-right:0}}@media (min-width: 1036px) and (max-width: 86rem){.l-fluid-breakout .l-fluid-breakout__aside--right{padding-left:1.5rem;padding-right:0}}@media (min-width: 86rem){.l-fluid-breakout .l-fluid-breakout__aside--right{padding-left:0;padding-right:1.5rem}}.p-heading-icon{margin-bottom:1rem}@media (min-width: 772px){.p-heading-icon{margin-bottom:0}}.p-heading-icon__header{display:flex}.p-heading-icon__header.is-stacked{display:inherit}.p-heading-icon__img{flex-shrink:0;height:2rem;margin-bottom:0;margin-right:1rem;width:2rem}@media (max-width: 772px){.p-heading-icon__img{margin-top:0.5rem}}@media (min-width: 772px){.p-heading-icon__img{height:2.5rem;margin-top:0.1rem;width:2.5rem}}.p-heading-icon--small .p-heading-icon__img{height:1.5rem;width:1.5rem}@media (max-width: 772px){.p-heading-icon--small .p-heading-icon__img{margin-top:.25rem}}@media (min-width: 772px){.p-heading-icon--small .p-heading-icon__img{height:2rem;margin-top:0;width:2rem}}h1 [class*='p-icon'],.p-heading--1 [class*='p-icon'],.p-heading--one [class*='p-icon'],.u-match-h1 [class*='p-icon']{background-size:contain;height:.517em;width:.517em;vertical-align:0}h2 [class*='p-icon'],.p-heading--2 [class*='p-icon'],.p-heading--two [class*='p-icon'],.u-match-h2 [class*='p-icon']{background-size:contain;height:.517em;width:.517em;vertical-align:0}h3 [class*='p-icon'],.p-heading--3 [class*='p-icon'],.p-heading--three [class*='p-icon'],.u-match-h3 [class*='p-icon']{background-size:contain;height:1rem;width:1rem;vertical-align:0}h4 [class*='p-icon'],.p-heading--4 [class*='p-icon'],.p-heading--four [class*='p-icon'],.u-match-h4 [class*='p-icon']{vertical-align:0}.p-icon--anchor{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 1a2.5 2.5 0 01.75 4.885v1.068h2.27v1.5H8.75v5.022c2.438-.161 4.172-1.077 4.172-1.669h1.5C14.422 13.57 11.547 15 8 15c-3.547 0-6.422-1.43-6.422-3.194h1.5c0 .592 1.734 1.508 4.172 1.67V8.452H4.904v-1.5H7.25V5.886A2.501 2.501 0 018 1zm0 1.5a1 1 0 100 2 1 1 0 000-2z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--anchor,.p-icon--anchor.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 1a2.5 2.5 0 01.75 4.885v1.068h2.27v1.5H8.75v5.022c2.438-.161 4.172-1.077 4.172-1.669h1.5C14.422 13.57 11.547 15 8 15c-3.547 0-6.422-1.43-6.422-3.194h1.5c0 .592 1.734 1.508 4.172 1.67V8.452H4.904v-1.5H7.25V5.886A2.501 2.501 0 018 1zm0 1.5a1 1 0 100 2 1 1 0 000-2z' fill='%23e5e5e5' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--plus{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23666' fill-rule='nonzero' d='M8.75 1.151V7.25l6.099.001v1.5h-6.1l.001 6.099h-1.5v-6.1L1.15 8.75v-1.5H7.25L7.25 1.15z'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--plus,.p-icon--plus.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23e5e5e5' fill-rule='nonzero' d='M8.75 1.151V7.25l6.099.001v1.5h-6.1l.001 6.099h-1.5v-6.1L1.15 8.75v-1.5H7.25L7.25 1.15z'/%3E%3C/svg%3E")}.p-icon--minus{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23666' fill-rule='nonzero' d='M14.849 7.25v1.5H1.15v-1.5z'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--minus,.p-icon--minus.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23e5e5e5' fill-rule='nonzero' d='M14.849 7.25v1.5H1.15v-1.5z'/%3E%3C/svg%3E")}.p-icon--expand{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 1a3 3 0 012.995 2.824L15 4v8a3 3 0 01-2.824 2.995L12 15H4a3 3 0 01-2.995-2.824L1 12V4a3 3 0 012.824-2.995L4 1h8zm0 1.5H4l-.144.007a1.5 1.5 0 00-1.35 1.349L2.5 4v8l.007.144a1.5 1.5 0 001.349 1.35L4 13.5h8l.144-.007a1.5 1.5 0 001.35-1.349L13.5 12V4l-.007-.144a1.5 1.5 0 00-1.349-1.35L12 2.5zm-1.25 3.372l1.061 1.06-3.784 3.786-3.785-3.785 1.06-1.06 2.724 2.723 2.725-2.724z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--expand,.p-icon--expand.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 1a3 3 0 012.995 2.824L15 4v8a3 3 0 01-2.824 2.995L12 15H4a3 3 0 01-2.995-2.824L1 12V4a3 3 0 012.824-2.995L4 1h8zm0 1.5H4l-.144.007a1.5 1.5 0 00-1.35 1.349L2.5 4v8l.007.144a1.5 1.5 0 001.349 1.35L4 13.5h8l.144-.007a1.5 1.5 0 001.35-1.349L13.5 12V4l-.007-.144a1.5 1.5 0 00-1.349-1.35L12 2.5zm-1.25 3.372l1.061 1.06-3.784 3.786-3.785-3.785 1.06-1.06 2.724 2.723 2.725-2.724z' fill='%23e5e5e5' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--collapse{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 1a3 3 0 013 3v8a3 3 0 01-3 3H4a3 3 0 01-3-3V4a3 3 0 013-3h8zm0 1.5H4a1.5 1.5 0 00-1.493 1.356L2.5 4v8a1.5 1.5 0 001.356 1.493L4 13.5h8a1.5 1.5 0 001.493-1.356L13.5 12V4a1.5 1.5 0 00-1.356-1.493L12 2.5zM8.027 5.282l3.76 3.76-1.06 1.061-2.701-2.7-2.699 2.7-1.06-1.06 3.76-3.76z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--collapse,.p-icon--collapse.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 1a3 3 0 013 3v8a3 3 0 01-3 3H4a3 3 0 01-3-3V4a3 3 0 013-3h8zm0 1.5H4a1.5 1.5 0 00-1.493 1.356L2.5 4v8a1.5 1.5 0 001.356 1.493L4 13.5h8a1.5 1.5 0 001.493-1.356L13.5 12V4a1.5 1.5 0 00-1.356-1.493L12 2.5zM8.027 5.282l3.76 3.76-1.06 1.061-2.701-2.7-2.699 2.7-1.06-1.06 3.76-3.76z' fill='%23e5e5e5' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--chevron-up{transform:rotate(180deg)}.p-icon--chevron-down,.p-icon--chevron-up{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23666' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--chevron-down,.p-icon--chevron-down.is-light,[class*='--dark'] .p-icon--chevron-up,.p-icon--chevron-up.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23e5e5e5' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E")}.p-icon--contextual-menu{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23666' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--contextual-menu,.p-icon--contextual-menu.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23e5e5e5' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E")}.p-icon--close{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23666' fill-rule='nonzero' d='M13.041 1.898l1.06 1.06L9.062 8l5.04 5.042-1.06 1.06L8 9.062 2.96 14.1l-1.06-1.06L6.938 8 1.9 2.96l1.06-1.06 5.04 5.04z'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--close,.p-icon--close.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23e5e5e5' fill-rule='nonzero' d='M13.041 1.898l1.06 1.06L9.062 8l5.04 5.042-1.06 1.06L8 9.062 2.96 14.1l-1.06-1.06L6.938 8 1.9 2.96l1.06-1.06 5.04 5.04z'/%3E%3C/svg%3E")}.p-icon--help,.p-icon--question{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 1a7 7 0 110 14A7 7 0 018 1zm0 1.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zm.5 7.5a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h1zm-.33-6.154c.448 0 .818.06 1.11.18.29.119.52.27.688.452.169.182.286.382.353.6.066.217.1.424.1.62s-.03.377-.09.542c-.06.165-.135.317-.226.457s-.196.274-.315.4l-.177.184-.434.442a2.493 2.493 0 00-.247.3 1.808 1.808 0 00-.184.331.92.92 0 00-.065.234l-.009.119v.126l.003.079.008.068H7.296l-.017-.112-.009-.124-.005-.227c0-.196.024-.373.073-.53.05-.159.114-.306.195-.443.08-.137.174-.264.279-.384l.16-.176.166-.17c.161-.162.309-.322.442-.48a.816.816 0 00.2-.541.565.565 0 00-.195-.432c-.13-.119-.32-.178-.573-.178-.253 0-.491.031-.716.094-.168.048-.35.117-.546.207l-.2.098-.442-1.188.17-.1c.234-.128.5-.23.797-.305a4.422 4.422 0 011.095-.143z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--help,.p-icon--help.is-light,[class*='--dark'] .p-icon--question,.p-icon--question.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 1a7 7 0 110 14A7 7 0 018 1zm0 1.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zm.5 7.5a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h1zm-.33-6.154c.448 0 .818.06 1.11.18.29.119.52.27.688.452.169.182.286.382.353.6.066.217.1.424.1.62s-.03.377-.09.542c-.06.165-.135.317-.226.457s-.196.274-.315.4l-.177.184-.434.442a2.493 2.493 0 00-.247.3 1.808 1.808 0 00-.184.331.92.92 0 00-.065.234l-.009.119v.126l.003.079.008.068H7.296l-.017-.112-.009-.124-.005-.227c0-.196.024-.373.073-.53.05-.159.114-.306.195-.443.08-.137.174-.264.279-.384l.16-.176.166-.17c.161-.162.309-.322.442-.48a.816.816 0 00.2-.541.565.565 0 00-.195-.432c-.13-.119-.32-.178-.573-.178-.253 0-.491.031-.716.094-.168.048-.35.117-.546.207l-.2.098-.442-1.188.17-.1c.234-.128.5-.23.797-.305a4.422 4.422 0 011.095-.143z' fill='%23e5e5e5' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--information{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 1a7 7 0 110 14A7 7 0 018 1zm0 1.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM8.75 7v5.02h-1.5V7h1.5zM8.5 3.944a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h1z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--information,.p-icon--information.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 1a7 7 0 110 14A7 7 0 018 1zm0 1.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM8.75 7v5.02h-1.5V7h1.5zM8.5 3.944a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h1z' fill='%23e5e5e5' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--delete{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.5 6v6a1.5 1.5 0 001.356 1.493L6 13.5h4a1.5 1.5 0 001.493-1.356L11.5 12V6H13v6a3 3 0 01-3 3H6a3 3 0 01-3-3V6h1.5zm3 0v5.994H6V6h1.5zm2.498 0v5.994h-1.5V6h1.5zM8.5 0A2.5 2.5 0 0111 2.5V3h3v1.5H2V3h3v-.5A2.5 2.5 0 017.5 0h1zm0 1.5h-1a1 1 0 00-.993.883L6.5 2.5V3h3v-.5a1 1 0 00-.883-.993L8.5 1.5z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--delete,.p-icon--delete.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.5 6v6a1.5 1.5 0 001.356 1.493L6 13.5h4a1.5 1.5 0 001.493-1.356L11.5 12V6H13v6a3 3 0 01-3 3H6a3 3 0 01-3-3V6h1.5zm3 0v5.994H6V6h1.5zm2.498 0v5.994h-1.5V6h1.5zM8.5 0A2.5 2.5 0 0111 2.5V3h3v1.5H2V3h3v-.5A2.5 2.5 0 017.5 0h1zm0 1.5h-1a1 1 0 00-.993.883L6.5 2.5V3h3v-.5a1 1 0 00-.883-.993L8.5 1.5z' fill='%23e5e5e5' fill-rule='evenodd'/%3E%3C/svg%3E")}.p-icon--error{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle stroke='%23C7162B' stroke-width='1.5' fill='%23c7162b' cx='8' cy='8' r='6.25'/%3E%3Cpath fill='%23FFF' fill-rule='nonzero' d='M10.282 4.638l1.06 1.06L9.05 7.99l2.293 2.292-1.06 1.06L7.99 9.05 5.7 11.343l-1.06-1.06 2.29-2.293L4.64 5.7l1.06-1.06 2.291 2.29z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--warning{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M9.34 1.2l5.842 11.627A1.5 1.5 0 0113.842 15H2.158a1.5 1.5 0 01-1.34-2.173L6.66 1.2a1.5 1.5 0 012.68 0z' fill='%23f99b11'/%3E%3Cpath d='M8.5 11a.5.5 0 01.492.41L9 11.5v1a.5.5 0 01-.41.492L8.5 13h-1a.5.5 0 01-.492-.41L7 12.5v-1a.5.5 0 01.41-.492L7.5 11h1zM9 5v4.5H7V5h2z' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--external-link{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.157 3l-1.5 1.5H6a.5.5 0 00-.492.41L5.5 5v5a.5.5 0 00.41.492L6 10.5h5a.5.5 0 00.492-.41L11.5 10V8.538l1.5-1.5V10a2 2 0 01-2 2H6a2 2 0 01-2-2V5a2 2 0 012-2h3.157zm5.593-1.75V6h-1.5V3.81L8.5 8.56 7.44 7.5l4.748-4.75H10v-1.5h4.75z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--external-link,.p-icon--external-link.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.157 3l-1.5 1.5H6a.5.5 0 00-.492.41L5.5 5v5a.5.5 0 00.41.492L6 10.5h5a.5.5 0 00.492-.41L11.5 10V8.538l1.5-1.5V10a2 2 0 01-2 2H6a2 2 0 01-2-2V5a2 2 0 012-2h3.157zm5.593-1.75V6h-1.5V3.81L8.5 8.56 7.44 7.5l4.748-4.75H10v-1.5h4.75z' fill='%23e5e5e5' fill-rule='evenodd'/%3E%3C/svg%3E")}.p-icon--drag{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 12.49a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm-6 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm6-5.99a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm-6 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm6-5.99a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm-6 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--drag,.p-icon--drag.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 12.49a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm-6 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm6-5.99a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm-6 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm6-5.99a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm-6 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3z' fill='%23e5e5e5' fill-rule='evenodd'/%3E%3C/svg%3E")}.p-icon--code{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.385 2.09v1.38l.27.028c.35.041.668.102.952.182.284.08.519.154.704.22l.129.05-.42 1.545-.18-.07a7.536 7.536 0 00-.848-.252A5.21 5.21 0 004.77 5.03c-.48 0-.812.085-.997.255a.79.79 0 00-.278.6c0 .16.032.293.097.398a.854.854 0 00.3.277c.135.08.298.158.488.233l.311.118.364.13c.39.15.75.307 1.08.472.33.165.615.355.855.57.24.215.428.467.563.757.135.29.202.63.202 1.02 0 .27-.04.538-.12.803a2.11 2.11 0 01-.397.735c-.186.225-.43.42-.736.585a3.275 3.275 0 01-.923.313l-.224.04v1.604h-1.71v-1.59l-.263-.028c-.51-.061-.93-.152-1.26-.272a6.716 6.716 0 01-.759-.319l-.103-.056.57-1.47.199.089c.273.116.582.224.926.324.43.125.885.187 1.365.187.56 0 .95-.072 1.17-.217.22-.145.33-.373.33-.683a.83.83 0 00-.128-.465 1.219 1.219 0 00-.375-.352 3 3 0 00-.607-.285L3.9 8.51c-.29-.1-.575-.215-.855-.345a3.35 3.35 0 01-.75-.472 2.14 2.14 0 01-.532-.675c-.135-.265-.203-.593-.203-.983 0-.28.037-.55.113-.81.075-.26.194-.5.36-.72.164-.22.382-.412.652-.577.216-.132.47-.24.763-.324l.227-.059V2.09h1.71zm9.603 9.16v1.5H8.982v-1.5h6.006z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--code,.p-icon--code.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.385 2.09v1.38l.27.028c.35.041.668.102.952.182.284.08.519.154.704.22l.129.05-.42 1.545-.18-.07a7.536 7.536 0 00-.848-.252A5.21 5.21 0 004.77 5.03c-.48 0-.812.085-.997.255a.79.79 0 00-.278.6c0 .16.032.293.097.398a.854.854 0 00.3.277c.135.08.298.158.488.233l.311.118.364.13c.39.15.75.307 1.08.472.33.165.615.355.855.57.24.215.428.467.563.757.135.29.202.63.202 1.02 0 .27-.04.538-.12.803a2.11 2.11 0 01-.397.735c-.186.225-.43.42-.736.585a3.275 3.275 0 01-.923.313l-.224.04v1.604h-1.71v-1.59l-.263-.028c-.51-.061-.93-.152-1.26-.272a6.716 6.716 0 01-.759-.319l-.103-.056.57-1.47.199.089c.273.116.582.224.926.324.43.125.885.187 1.365.187.56 0 .95-.072 1.17-.217.22-.145.33-.373.33-.683a.83.83 0 00-.128-.465 1.219 1.219 0 00-.375-.352 3 3 0 00-.607-.285L3.9 8.51c-.29-.1-.575-.215-.855-.345a3.35 3.35 0 01-.75-.472 2.14 2.14 0 01-.532-.675c-.135-.265-.203-.593-.203-.983 0-.28.037-.55.113-.81.075-.26.194-.5.36-.72.164-.22.382-.412.652-.577.216-.132.47-.24.763-.324l.227-.059V2.09h1.71zm9.603 9.16v1.5H8.982v-1.5h6.006z' fill='%23e5e5e5' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--menu{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14 12.25v1.5H2v-1.5h12zm0-5v1.5H2v-1.5h12zm0-5v1.5H2v-1.5h12z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--menu,.p-icon--menu.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14 12.25v1.5H2v-1.5h12zm0-5v1.5H2v-1.5h12zm0-5v1.5H2v-1.5h12z' fill='%23e5e5e5' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--copy{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13.731 10v2.274h2.275v1.5h-2.275v2.232h-1.5v-2.232H10v-1.5h2.231V10h1.5zM11 4.948H5V3.5H3.5v10h5V15h-5A1.5 1.5 0 012 13.5v-10A1.5 1.5 0 013.5 2h1.67a3.001 3.001 0 015.66 0h1.67A1.5 1.5 0 0114 3.5v3.709h-1.5V3.5H11v1.448zM8 1.5a1.5 1.5 0 00-1.493 1.356L6.5 3v.447h3V3a1.5 1.5 0 00-1.356-1.493L8 1.5z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--copy,.p-icon--copy.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13.731 10v2.274h2.275v1.5h-2.275v2.232h-1.5v-2.232H10v-1.5h2.231V10h1.5zM11 4.948H5V3.5H3.5v10h5V15h-5A1.5 1.5 0 012 13.5v-10A1.5 1.5 0 013.5 2h1.67a3.001 3.001 0 015.66 0h1.67A1.5 1.5 0 0114 3.5v3.709h-1.5V3.5H11v1.448zM8 1.5a1.5 1.5 0 00-1.493 1.356L6.5 3v.447h3V3a1.5 1.5 0 00-1.356-1.493L8 1.5z' fill='%23e5e5e5' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--search{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.964 1a5.964 5.964 0 014.709 9.623l4.303 4.305-1.06 1.06-4.306-4.305A5.964 5.964 0 116.963 1zm0 1.5a4.464 4.464 0 100 8.927 4.464 4.464 0 000-8.927z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--search,.p-icon--search.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.964 1a5.964 5.964 0 014.709 9.623l4.303 4.305-1.06 1.06-4.306-4.305A5.964 5.964 0 116.963 1zm0 1.5a4.464 4.464 0 100 8.927 4.464 4.464 0 000-8.927z' fill='%23e5e5e5' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--success{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cg fill='none' fill-rule='nonzero'%3E%3Cpath fill='%230e8620' d='M8 1a7 7 0 110 14A7 7 0 018 1zm2.83 3.502L6.863 9.884 5.174 8.096l-1.09 1.03 2.92 3.096 5.034-6.83-1.208-.89z'/%3E%3Cpath fill='%23fff' d='M10.83 4.502l1.208.89-5.033 6.83-2.922-3.096 1.091-1.03 1.689 1.789z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--share{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 0a3 3 0 11-2.123 5.12L6.869 7.12a3 3 0 01-.029 1.848l3.058 1.89a3 3 0 11-.774 1.285l-3.109-1.922a3 3 0 11.068-4.381l3.032-2.017A3.002 3.002 0 0112 0zm0 11.5a1.5 1.5 0 100 3 1.5 1.5 0 000-3zm-8-5a1.5 1.5 0 100 3 1.5 1.5 0 000-3zm8-5a1.5 1.5 0 100 3 1.5 1.5 0 000-3z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--share,.p-icon--share.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 0a3 3 0 11-2.123 5.12L6.869 7.12a3 3 0 01-.029 1.848l3.058 1.89a3 3 0 11-.774 1.285l-3.109-1.922a3 3 0 11.068-4.381l3.032-2.017A3.002 3.002 0 0112 0zm0 11.5a1.5 1.5 0 100 3 1.5 1.5 0 000-3zm-8-5a1.5 1.5 0 100 3 1.5 1.5 0 000-3zm8-5a1.5 1.5 0 100 3 1.5 1.5 0 000-3z' fill='%23e5e5e5' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--user{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 1a4 4 0 014 4v.5a3.987 3.987 0 01-1.139 2.795 6 6 0 014.135 5.48L15 14H1a6.003 6.003 0 014.14-5.706A3.98 3.98 0 014 5.5V5a4 4 0 014-4zm1 8.5H7a4.502 4.502 0 00-4.203 2.888l-.04.112h10.486l-.03-.084a4.504 4.504 0 00-4-2.911L9 9.5zm-1-7a2.5 2.5 0 00-2.495 2.336L5.5 5v.5a2.5 2.5 0 004.995.164L10.5 5.5V5A2.5 2.5 0 008 2.5z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--user,.p-icon--user.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 1a4 4 0 014 4v.5a3.987 3.987 0 01-1.139 2.795 6 6 0 014.135 5.48L15 14H1a6.003 6.003 0 014.14-5.706A3.98 3.98 0 014 5.5V5a4 4 0 014-4zm1 8.5H7a4.502 4.502 0 00-4.203 2.888l-.04.112h10.486l-.03-.084a4.504 4.504 0 00-4-2.911L9 9.5zm-1-7a2.5 2.5 0 00-2.495 2.336L5.5 5v.5a2.5 2.5 0 004.995.164L10.5 5.5V5A2.5 2.5 0 008 2.5z' fill='%23e5e5e5' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--spinner{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 13.5a5.488 5.488 0 004.183-1.929l1.317.76A6.988 6.988 0 018 15a6.988 6.988 0 01-5.5-2.669l1.316-.76A5.488 5.488 0 008 13.5zM6.999 1.071v1.52A5.502 5.502 0 002.815 9.84L1.5 10.6A7.002 7.002 0 016.764 1.11l.235-.038zM15 8c0 .918-.177 1.795-.498 2.6l-1.317-.761A5.502 5.502 0 009 2.59V1.07c3.392.485 6 3.403 6 6.929z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}[class*='--dark'] .p-icon--spinner,.p-icon--spinner.is-light{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 13.5a5.488 5.488 0 004.183-1.929l1.317.76A6.988 6.988 0 018 15a6.988 6.988 0 01-5.5-2.669l1.316-.76A5.488 5.488 0 008 13.5zM6.999 1.071v1.52A5.502 5.502 0 002.815 9.84L1.5 10.6A7.002 7.002 0 016.764 1.11l.235-.038zM15 8c0 .918-.177 1.795-.498 2.6l-1.317-.761A5.502 5.502 0 009 2.59V1.07c3.392.485 6 3.403 6 6.929z' fill='%23e5e5e5' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--facebook{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='40' height='40'%3E%3Cdefs%3E%3Cpath id='a' d='M.002.002H40v39.755H.002z'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cmask id='b' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3C/mask%3E%3Cpath fill='%231877F2' d='M40 20C40 8.954 31.046 0 20 0S0 8.954 0 20c0 9.983 7.314 18.257 16.875 19.757V25.781h-5.078V20h5.078v-4.406c0-5.013 2.986-7.781 7.554-7.781 2.188 0 4.477.39 4.477.39v4.922h-2.522c-2.484 0-3.259 1.542-3.259 3.123V20h5.547l-.887 5.781h-4.66v13.976C32.686 38.257 40 29.983 40 20' mask='url(%23b)'/%3E%3Cpath fill='%23FFF' d='M27.785 25.781L28.672 20h-5.547v-3.752c0-1.581.775-3.123 3.26-3.123h2.521V8.203s-2.289-.39-4.477-.39c-4.568 0-7.554 2.768-7.554 7.78V20h-5.078v5.781h5.078v13.976a20.15 20.15 0 006.25 0V25.781h4.66'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--twitter{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg fill='none'%3E%3Ccircle cx='20' cy='20' r='20' fill='%231DA1F2'/%3E%3Cpath fill='%23FFF' d='M16.34 30.55c8.87 0 13.72-7.35 13.72-13.72 0-.21 0-.42-.01-.62.94-.68 1.76-1.53 2.41-2.5-.86.38-1.79.64-2.77.76 1-.6 1.76-1.54 2.12-2.67-.93.55-1.96.95-3.06 1.17a4.799 4.799 0 00-3.52-1.52c-2.66 0-4.82 2.16-4.82 4.82 0 .38.04.75.13 1.1a13.68 13.68 0 01-9.94-5.04c-.41.71-.65 1.54-.65 2.42a4.8 4.8 0 002.15 4.01c-.79-.02-1.53-.24-2.18-.6v.06c0 2.34 1.66 4.28 3.87 4.73a4.807 4.807 0 01-2.18.08 4.815 4.815 0 004.5 3.35 9.693 9.693 0 01-7.14 1.99c2.11 1.38 4.65 2.18 7.37 2.18'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--instagram{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cdefs%3E%3ClinearGradient id='a' x1='50%25' x2='50%25' y1='99.709%25' y2='.777%25'%3E%3Cstop offset='0%25' stop-color='%23E09B3D'/%3E%3Cstop offset='30%25' stop-color='%23C74C4D'/%3E%3Cstop offset='60%25' stop-color='%23C21975'/%3E%3Cstop offset='100%25' stop-color='%237024C4'/%3E%3C/linearGradient%3E%3ClinearGradient id='b' x1='50%25' x2='50%25' y1='146.099%25' y2='-45.16%25'%3E%3Cstop offset='0%25' stop-color='%23E09B3D'/%3E%3Cstop offset='30%25' stop-color='%23C74C4D'/%3E%3Cstop offset='60%25' stop-color='%23C21975'/%3E%3Cstop offset='100%25' stop-color='%237024C4'/%3E%3C/linearGradient%3E%3ClinearGradient id='c' x1='50%25' x2='50%25' y1='658.141%25' y2='-140.029%25'%3E%3Cstop offset='0%25' stop-color='%23E09B3D'/%3E%3Cstop offset='30%25' stop-color='%23C74C4D'/%3E%3Cstop offset='60%25' stop-color='%23C21975'/%3E%3Cstop offset='100%25' stop-color='%237024C4'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cg fill='none'%3E%3Cpath fill='url(%23a)' d='M28.035 0h-16.14C5.336 0 0 5.336 0 11.895v16.14C0 34.594 5.336 39.93 11.895 39.93h16.14c6.559 0 11.895-5.336 11.895-11.895v-16.14C39.93 5.336 34.594 0 28.035 0zm7.878 28.035a7.878 7.878 0 01-7.878 7.878h-16.14a7.878 7.878 0 01-7.878-7.878v-16.14a7.878 7.878 0 017.878-7.878h16.14a7.878 7.878 0 017.878 7.878v16.14z'/%3E%3Cpath fill='url(%23b)' d='M19.965 9.638c-5.694 0-10.327 4.633-10.327 10.327s4.633 10.327 10.327 10.327 10.327-4.633 10.327-10.327c0-5.695-4.633-10.327-10.327-10.327zm0 16.637a6.31 6.31 0 110-12.62 6.31 6.31 0 010 12.62z'/%3E%3Ccircle cx='30.312' cy='9.715' r='2.475' fill='url(%23c)'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--linkedin{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath fill='%232867B2' fill-rule='nonzero' d='M2.956 0h34.088C38.677 0 40 1.283 40 2.865v34.27C40 38.717 38.677 40 37.044 40H2.956C1.323 40 0 38.717 0 37.135V2.865C0 1.283 1.323 0 2.956 0z'/%3E%3Cpath fill='%23FFF' d='M12.146 34.5V15H6v19.5zM9.054 12.65c2.216 0 3.596-1.589 3.596-3.575-.041-2.03-1.38-3.575-3.554-3.575S5.5 7.045 5.5 9.075c0 1.986 1.38 3.575 3.512 3.575h.042zM21.724 34.5V23.587c0-.584.04-1.167.205-1.585.45-1.167 1.474-2.375 3.194-2.375 2.252 0 3.153 1.792 3.153 4.419V34.5H34.5V23.295c0-6.002-3.07-8.795-7.166-8.795-3.358 0-4.832 1.959-5.651 3.293h.042v-2.834H15.5c.082 1.833 0 19.541 0 19.541h6.224z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--youtube{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='40' height='40'%3E%3Cdefs%3E%3Cpath id='a' d='M.014.009H40v28.173H.014z'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd' transform='translate(0 6)'%3E%3Cmask id='b' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3C/mask%3E%3Cpath fill='%23DA322A' d='M39.164 4.4A5.026 5.026 0 0035.628.842C32.508 0 20 0 20 0S7.492 0 4.372.841a5.026 5.026 0 00-3.536 3.56C0 7.54 0 14.09 0 14.09s0 6.55.836 9.69a5.026 5.026 0 003.536 3.56c3.12.84 15.628.84 15.628.84s12.508 0 15.628-.84a5.026 5.026 0 003.536-3.56c.836-3.14.836-9.69.836-9.69s0-6.55-.836-9.69' mask='url(%23b)'/%3E%3Cpath fill='%23FFFFFE' d='M15.909 20.038V8.143l10.455 5.948-10.455 5.947'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--canonical{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath fill='%23772953' d='M20 32.735c-7.033 0-12.735-5.7-12.735-12.735 0-7.034 5.702-12.735 12.735-12.735 7.034 0 12.736 5.701 12.736 12.735 0 7.035-5.701 12.735-12.735 12.735zM40 20c0 11.046-8.954 20-20 20S0 31.045 0 20C0 8.954 8.954 0 20 0s20 8.954 20 20zM20 4.865C11.64 4.865 4.865 11.641 4.865 20c0 8.36 6.776 15.135 15.135 15.135 8.36 0 15.135-6.775 15.135-15.135 0-8.358-6.775-15.135-15.135-15.135z'/%3E%3C/svg%3E")}.p-icon--ubuntu{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg fill='none'%3E%3Cpath fill='%23E95420' d='M40 20.04c0 11.012-8.95 19.95-20 19.95S0 31.052 0 20.04C0 9.017 8.95.09 20 .09s20 8.927 20 19.95z'/%3E%3Cpath fill='%23FFF' d='M6.4 17.377a2.666 2.666 0 00-2.67 2.663c0 1.466 1.2 2.663 2.67 2.663s2.67-1.197 2.67-2.663c0-1.476-1.2-2.663-2.67-2.663zm19.07 12.1a2.667 2.667 0 102.67 4.618 2.667 2.667 0 00.98-3.641c-.75-1.267-2.38-1.706-3.65-.978zM12.2 20.04a7.749 7.749 0 013.32-6.364l-1.95-3.262a11.622 11.622 0 00-4.8 6.723 3.751 3.751 0 011.38 2.903c0 1.167-.54 2.214-1.38 2.903a11.578 11.578 0 004.8 6.723l1.95-3.262a7.749 7.749 0 01-3.32-6.364zm7.8-7.78c4.08 0 7.42 3.112 7.77 7.092l3.81-.06a11.503 11.503 0 00-3.45-7.501c-1.02.379-2.19.319-3.2-.26a3.737 3.737 0 01-1.83-2.643 11.8 11.8 0 00-3.1-.42c-1.85 0-3.59.43-5.14 1.198l1.86 3.312a7.81 7.81 0 013.28-.719zm0 15.56a7.89 7.89 0 01-3.29-.718l-1.86 3.312c1.55.768 3.3 1.197 5.14 1.197 1.07 0 2.11-.15 3.1-.419a3.728 3.728 0 011.83-2.643 3.742 3.742 0 013.2-.26 11.551 11.551 0 003.45-7.501l-3.81-.06c-.34 3.97-3.68 7.092-7.76 7.092zm5.46-17.226c1.28.738 2.91.299 3.65-.978.74-1.277.3-2.903-.98-3.64a2.673 2.673 0 00-3.65.977 2.676 2.676 0 00.98 3.64z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--rss{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg fill='none'%3E%3Crect width='40' height='40' fill='%23EA7819' rx='5' transform='rotate(180 20 20)'/%3E%3Cpath fill='%23FFF' d='M6.34 6.274c15.07 0 27.332 12.314 27.332 27.452H28.41c0-12.236-9.9-22.19-22.07-22.19zM6.334 15.6c9.95 0 18.044 8.128 18.044 18.119h-5.261c0-3.44-1.33-6.671-3.747-9.097a12.657 12.657 0 00-9.036-3.76zm3.639 10.805a3.645 3.645 0 110 7.29 3.645 3.645 0 010-7.29z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--email{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg fill='none'%3E%3Ccircle cx='20' cy='20' r='20' fill='%23666'/%3E%3Cpath fill='%23FFF' d='M13.688 20.68a.312.312 0 01.432 0l2.888 2.752A4.344 4.344 0 0020 24.624l.238-.006a4.344 4.344 0 002.754-1.186l2.864-2.752a.312.312 0 01.432 0l7.92 7.92a.312.312 0 01-.224.528h-28a.312.312 0 01-.216-.528zM33.8 13.184a.304.304 0 01.512.224V26.52a.304.304 0 01-.52.224l-6.664-6.728a.304.304 0 010-.432zm-27.608 0l6.664 6.4a.296.296 0 010 .432l-6.664 6.728a.304.304 0 01-.52-.224V13.408a.312.312 0 01.52-.224zm27.696-2.328a.352.352 0 01.24.608L22.544 22.496A3.688 3.688 0 0120 23.512l-.218-.006a3.656 3.656 0 01-2.326-1.01L5.864 11.464a.352.352 0 01.24-.608z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--medium{background-size:contain;height:1.25rem;width:1.25rem}.p-icon--large{background-size:contain;height:1.5rem;width:1.5rem}.p-icon--x-large{background-size:contain;height:2.25rem;width:2.25rem}.p-icon--xx-large{background-size:contain;height:3rem;width:3rem}[class*='p-button-'] [class*='p-icon-']{top:0}.p-image--bordered{border-color:#cdcdcd;border-style:solid;border-width:1px}.p-image--shadowed{box-shadow:0 1px 5px 1px rgba(205,205,205,0.2)}.p-inline-images{display:block;list-style:none;margin-left:0;padding-left:0;text-align:center}.p-inline-images__item{display:inline-block;margin:1rem;overflow:hidden;text-align:center;vertical-align:middle}@media only screen and (min-width: 772px){.p-inline-images__item{margin:1.875rem}}.p-inline-images__logo{max-height:3rem;max-width:7rem;width:auto}@media screen and (min-width: 772px){.p-inline-images__logo{max-height:5.5rem;max-width:9rem}}.p-label--validated,.p-label--in-progress,.p-label--new,.p-label--updated,.p-label--deprecated{border-radius:.125rem;display:inline-block;font-weight:400;padding:0.25rem .5rem;text-align:center;text-decoration:none;white-space:nowrap}.p-label--validated{background-color:#006b75;color:#fff}.p-label--in-progress{background-color:#f99b11;color:#111}.p-label--new{background-color:#0e8620;color:#fff}.p-label--updated{background-color:#24598f;color:#fff}.p-label--deprecated{background-color:#c7162b;color:#fff}.p-link--soft{color:#111}.p-link--soft:visited{color:#111;text-decoration:none}.p-link--soft:hover{color:#06c}.p-link--soft.is-selected{font-weight:400}.p-link--inverted{color:#f7f7f7;font-weight:400}.p-link--inverted:hover{color:#f7f7f7}.p-link--inverted:visited{color:#dedede}@supports ((-webkit-mask-size: 1em) or (mask-size: 1em)) or (-webkit-mask-size: 1em){.p-link--external::after{background-color:currentColor;content:'';display:inline-block;height:1rem;line-height:1;-webkit-mask:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.157 3l-1.5 1.5H6a.5.5 0 00-.492.41L5.5 5v5a.5.5 0 00.41.492L6 10.5h5a.5.5 0 00.492-.41L11.5 10V8.538l1.5-1.5V10a2 2 0 01-2 2H6a2 2 0 01-2-2V5a2 2 0 012-2h3.157zm5.593-1.75V6h-1.5V3.81L8.5 8.56 7.44 7.5l4.748-4.75H10v-1.5h4.75z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E") no-repeat 0 0 / 1rem;mask:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.157 3l-1.5 1.5H6a.5.5 0 00-.492.41L5.5 5v5a.5.5 0 00.41.492L6 10.5h5a.5.5 0 00.492-.41L11.5 10V8.538l1.5-1.5V10a2 2 0 01-2 2H6a2 2 0 01-2-2V5a2 2 0 012-2h3.157zm5.593-1.75V6h-1.5V3.81L8.5 8.56 7.44 7.5l4.748-4.75H10v-1.5h4.75z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E") no-repeat 0 0 / 1rem;vertical-align:calc(.932em - 1rem - 1px);width:1rem}}.p-top{border-bottom:1px dotted #cdcdcd;clear:both;margin:20px 0}.p-top__link{background:#fff;color:#111;float:right;margin-right:5px;padding:0 5px;position:relative;text-decoration:none;top:-0.725rem}.p-list-tree .p-list-tree[aria-hidden='false']::after,.p-list-tree__item--group::after{background-position:center;background-repeat:no-repeat;content:' ';display:block;height:.9375rem;left:-0.75rem;pointer-events:none;position:absolute;top:0.4rem;width:.9375rem}.p-list-tree{border-left:1px solid #cdcdcd;list-style-type:none;margin-left:1rem;padding:0 0 0 .25rem}.p-list-tree .p-list-tree{display:none;margin-left:0}.p-list-tree .p-list-tree[aria-hidden='false']{display:block}.p-list-tree .p-list-tree[aria-hidden='false']::after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='15' width='15' viewBox='0 0 15 15'%3E%3Cdefs%3E%3Cpath id='a' d='M0 0h15v15H0z'/%3E%3C/defs%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cuse xlink:href='%23a' fill='%23FFF'/%3E%3Cpath stroke='%23888' d='M.5.5h14v14H.5z'/%3E%3Cpath fill='%23888' d='M4 8V7h7v1z'/%3E%3C/g%3E%3C/svg%3E");z-index:1}.p-list-tree__item{margin-top:.125rem;padding-left:0.8rem;position:relative}.p-list-tree__item:focus{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-list-tree__item:focus-visible{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-list-tree__item:focus:not(:focus-visible){outline:0;outline-offset:0}.p-list-tree__item::before{background:#cdcdcd;content:' ';display:block;height:1px;left:-.25rem;pointer-events:none;position:absolute;top:0.8rem;width:0.625rem}.p-list-tree__item--group::after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='15' width='15' viewBox='0 0 15 15'%3E%3Cdefs%3E%3Cpath id='a' d='M0 0h15v15H0z'/%3E%3C/defs%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cuse xlink:href='%23a' fill='%23FFF'/%3E%3Cpath stroke='%23888' d='M.5.5h14v14H.5z'/%3E%3Cpath fill='%23888' d='M7 4h1v7H7z'/%3E%3Cpath fill='%23888' d='M4 8V7h7v1z'/%3E%3C/g%3E%3C/svg%3E")}.p-list-tree__toggle{background:transparent;border:0;font-weight:normal;margin:0 0.5rem 0 -1.75rem;padding:0 0.5rem 0 1.75rem;transition-duration:0s;width:auto}.p-list-tree__toggle:focus{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-list-tree__toggle:focus-visible{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-list-tree__toggle:focus:not(:focus-visible){outline:0;outline-offset:0}.p-list-tree__toggle:hover{background:transparent;color:#06c;text-decoration:underline}.p-stepped-list,.p-stepped-list--detailed{counter-reset:li;display:flex;flex-direction:column;list-style:none;padding-left:0}.p-stepped-list__title{display:flex;list-style:none;margin-left:0;padding-left:0}.p-stepped-list__title::before{align-self:start;background-color:#e5e5e5;border-radius:100%;content:counter(li);counter-increment:li;direction:rtl;display:block;text-align:center}.p-list__item,.p-list--divided .p-list__item{padding-bottom:.25rem;padding-top:.25rem}form .p-list__item,form .p-list--divided .p-list__item,.p-list--divided form .p-list__item{padding-bottom:0;padding-top:0}form .p-list__item label,form .p-list--divided .p-list__item label,.p-list--divided form .p-list__item label{margin-bottom:.1rem}.p-list--divided .p-list__item{position:relative}.p-list--divided .p-list__item::after{border-bottom:1px solid #cdcdcd;bottom:0;content:'';height:1px;left:0;position:absolute;right:0}.p-list--divided .p-list__item:last-of-type::after,.p-list--divided .p-list__item .last-item::after{border-bottom:0}.p-list--divided.is-split .p-list__item:last-of-type{border-bottom:1px solid #cdcdcd}.is-ticked{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cg fill='none' fill-rule='nonzero'%3E%3Cpath fill='%23333' d='M8 1a7 7 0 110 14A7 7 0 018 1zm2.83 3.502L6.863 9.884 5.174 8.096l-1.09 1.03 2.92 3.096 5.034-6.83-1.208-.89z'/%3E%3Cpath fill='%23fff' d='M10.83 4.502l1.208.89-5.033 6.83-2.922-3.096 1.091-1.03 1.689 1.789z'/%3E%3C/g%3E%3C/svg%3E");background-position-y:.55rem;background-repeat:no-repeat;background-size:.875rem;padding-left:2rem}.p-inline-list{margin-left:0;padding-left:0}.p-inline-list__item{display:inline;list-style:none;margin-right:1rem}.p-inline-list__item.last-item,.p-inline-list__item:last-of-type{margin-right:0}.p-inline-list--middot{margin-left:0;padding-left:0}.p-inline-list--middot .p-inline-list__item{display:inline;list-style:none;margin-right:1rem;position:relative}.p-inline-list--middot .p-inline-list__item.last-item,.p-inline-list--middot .p-inline-list__item:last-of-type{margin-right:0}.p-inline-list--middot .p-inline-list__item::after{color:#666;content:'\00b7';font-size:1.4em;line-height:0;position:absolute;right:-0.5rem;top:0.4em}.p-inline-list--middot .p-inline-list__item:hover::after{color:#666}.p-inline-list--middot .p-inline-list__item:last-of-type::after,.p-inline-list--middot .p-inline-list__item .last-item::after{content:''}.p-inline-list--stretch{display:flex;flex-wrap:wrap;margin-left:0;padding-left:0}.p-inline-list--stretch .p-inline-list__item{flex:1 auto;list-style:none}.p-stepped-list{margin-bottom:0;margin-left:0}.p-stepped-list__item{float:none;margin-left:0;overflow:visible;padding-bottom:1.5rem;position:relative;width:100%}.p-stepped-list__bullet{display:none}h6.p-stepped-list__title::before{flex-shrink:0;margin-right:1rem}@media (max-width: 772px){h6.p-stepped-list__title::before{width:1.5rem}}@media (min-width: 772px){h6.p-stepped-list__title::before{width:1.5rem}}@media (max-width: 772px){h6.p-stepped-list__title+.p-stepped-list__content{margin-left:2.5rem}}@media (min-width: 772px){h6.p-stepped-list__title+.p-stepped-list__content{margin-left:2.5rem}}h5.p-stepped-list__title::before{flex-shrink:0;margin-right:1rem}@media (max-width: 772px){h5.p-stepped-list__title::before{width:1.5rem}}@media (min-width: 772px){h5.p-stepped-list__title::before{width:1.5rem}}@media (max-width: 772px){h5.p-stepped-list__title+.p-stepped-list__content{margin-left:2.5rem}}@media (min-width: 772px){h5.p-stepped-list__title+.p-stepped-list__content{margin-left:2.5rem}}h4.p-stepped-list__title::before{flex-shrink:0;margin-right:1rem}@media (max-width: 772px){h4.p-stepped-list__title::before{width:1.5rem}}@media (min-width: 772px){h4.p-stepped-list__title::before{width:2rem}}@media (max-width: 772px){h4.p-stepped-list__title+.p-stepped-list__content{margin-left:2.5rem}}@media (min-width: 772px){h4.p-stepped-list__title+.p-stepped-list__content{margin-left:3rem}}h3.p-stepped-list__title::before{flex-shrink:0;margin-right:1rem}@media (max-width: 772px){h3.p-stepped-list__title::before{width:2rem}}@media (min-width: 772px){h3.p-stepped-list__title::before{width:2.5rem}}@media (max-width: 772px){h3.p-stepped-list__title+.p-stepped-list__content{margin-left:3rem}}@media (min-width: 772px){h3.p-stepped-list__title+.p-stepped-list__content{margin-left:3.5rem}}h2.p-stepped-list__title::before{flex-shrink:0;margin-right:1rem}@media (max-width: 772px){h2.p-stepped-list__title::before{width:2.5rem}}@media (min-width: 772px){h2.p-stepped-list__title::before{width:3rem}}@media (max-width: 772px){h2.p-stepped-list__title+.p-stepped-list__content{margin-left:3.5rem}}@media (min-width: 772px){h2.p-stepped-list__title+.p-stepped-list__content{margin-left:4rem}}h1.p-stepped-list__title::before{flex-shrink:0;margin-right:1rem}@media (max-width: 772px){h1.p-stepped-list__title::before{width:3rem}}@media (min-width: 772px){h1.p-stepped-list__title::before{width:3.5rem}}@media (max-width: 772px){h1.p-stepped-list__title+.p-stepped-list__content{margin-left:4rem}}@media (min-width: 772px){h1.p-stepped-list__title+.p-stepped-list__content{margin-left:4.5rem}}@media (min-width: 772px){.p-stepped-list--detailed .p-stepped-list__title+.p-stepped-list__content{margin-left:0}}.p-stepped-list--detailed{margin-left:auto}@media (min-width: 772px){.p-stepped-list--detailed .p-stepped-list__content,.p-stepped-list--detailed .p-stepped-list__title{flex-basis:0;flex-grow:1;flex-shrink:1;margin-left:2rem}.p-stepped-list--detailed .p-stepped-list__content:first-child,.p-stepped-list--detailed .p-stepped-list__title:first-child{margin-left:0}}@supports (display: grid){@media (min-width: 772px){.p-stepped-list--detailed .p-stepped-list__content{grid-column-end:span 6;margin-top:0}}.p-stepped-list--detailed .p-stepped-list__title{display:flex;grid-column-end:span 6;margin-left:0}}.p-stepped-list--detailed .p-stepped-list__item{margin-left:0;margin-right:0;padding-left:0;padding-right:0}@media (min-width: 772px){.p-stepped-list--detailed .p-stepped-list__item{padding-top:.5rem;position:relative}.p-stepped-list--detailed .p-stepped-list__item::after{background-color:rgba(0,0,0,0.1);content:'';height:1px;left:0;position:absolute;right:0;top:0}}@media (min-width: 772px){@supports ((-moz-columns: 1) or (columns: 1)){[class*='p-list'].is-split{-moz-column-gap:2rem;column-gap:2rem;-moz-columns:2;columns:2}[class*='p-list'].is-split .p-list__item{display:inline-block;width:100%}}@supports not ((-moz-columns: 1) or (columns: 1)){[class*='p-list'].is-split{display:flex;flex-wrap:wrap}[class*='p-list'].is-split .p-list__item{width:calc(50% - 0.5rem)}}[class*='p-list'].is-split:nth-child(2n-1){margin-right:1rem}}.p-matrix{display:flex;flex-wrap:wrap;list-style:none;margin-bottom:1.5rem;margin-left:0;padding-left:0}.p-matrix>p:last-child{margin-bottom:.1rem}.p-matrix__item{border-top:1px solid #cdcdcd;display:flex;flex:1 1 auto;padding-bottom:1rem;padding-top:calc(1rem - 1px)}.p-matrix__item:first-child{border-top:none}@media (min-width: 620px){.p-matrix__item{display:flex;flex-wrap:wrap;width:33.333%}}@media (min-width: 620px) and (max-width: 1036px){.p-matrix__item{flex-direction:column}}@media (min-width: 772px){.p-matrix__item{border-right:1px solid #cdcdcd;padding-left:1rem;padding-right:1rem;width:33.333%}.p-matrix__item:empty{display:block}.p-matrix__item:nth-child(3n+1){padding-left:0}.p-matrix__item:nth-child(3n+3){border-right:0}.p-matrix__item:nth-child(1),.p-matrix__item:nth-child(2),.p-matrix__item:nth-child(3){border-top:0}}@media (min-width: 1036px){.p-matrix__item{border-right:1px solid #cdcdcd;width:33.333%}.p-matrix__item:empty{display:block}.p-matrix__item:nth-child(3n+1){padding-left:0}.p-matrix__item:nth-child(3n+3){border-right:0;padding-right:0}.p-matrix__item:nth-last-child(1),.p-matrix__item:nth-last-child(2),.p-matrix__item:nth-last-child(3){border-bottom:0}}.p-matrix__img{align-self:flex-start;flex-shrink:0;height:auto;margin-bottom:.5rem;margin-right:1rem;width:2rem}@media (max-width: 772px){.p-matrix__img{margin-top:0}}@media (min-width: 772px){.p-matrix__img{margin-top:-0.05rem}}@media (min-width: 1681px){.p-matrix__img{margin-top:-0}}.p-matrix__content{display:flex;flex:1 1 auto;flex-direction:column;padding-right:1rem}@media (min-width: 1036px){.p-matrix__content{width:calc(100% - 3rem)}}@media (min-width: 772px){.p-matrix__title{margin-bottom:-0.05rem}}@media (min-width: 1681px){.p-matrix__title{margin-bottom:-0}}@media (max-width: 772px){.p-matrix__desc{margin-top:-.5rem}}.p-media-object,.p-media-object--large{display:flex;flex-shrink:0;margin-bottom:1.5rem}.p-media-object__meta-list-item--date,.p-media-object__meta-list-item--location,.p-media-object__meta-list-item--venue,.p-media-object__meta-list-item{color:#111;padding-left:2rem}.p-media-object__meta-list-item--date,.p-media-object__meta-list-item--location,.p-media-object__meta-list-item--venue{background-position:0 75%;background-repeat:no-repeat;background-size:1rem}.p-media-object__image{align-self:flex-start;border-radius:.125rem;flex-basis:inherit;flex-shrink:0;margin-right:1rem;max-height:3rem;max-width:3rem;vertical-align:middle;width:auto}.p-media-object__content{margin-bottom:.6rem;margin-top:0}.p-media-object__image.is-round{border-radius:50%}.p-media-object__meta-list{list-style:none;margin:0;padding-left:0;padding-top:.5rem}.p-media-object__meta-list-item--date{background-image:url('data:image/svg+xml;utf8,')}.p-media-object__meta-list-item--location{background-image:url('data:image/svg+xml;utf8,')}.p-media-object__meta-list-item--venue{background-image:url('data:image/svg+xml;utf8,')}.p-media-object--large .p-media-object__image{max-height:6rem;max-width:6rem}.p-modal{align-items:center;background:rgba(17,17,17,0.85);bottom:0;display:flex;height:100vh;justify-content:center;left:0;margin:0;padding:1rem;position:fixed;right:0;top:0;width:100%;z-index:101}.p-modal__dialog{bottom:1rem;left:1.5rem;margin-bottom:0;max-height:calc(100% - 2rem);max-width:72rem;overflow:scroll;position:absolute;right:1.5rem;top:1rem;width:auto}@media screen and (min-width: 772px){.p-modal__dialog{bottom:initial;left:initial;position:relative;right:initial;top:initial}}.p-modal__header{display:flex;justify-content:space-between;margin-bottom:.5rem}.p-modal__title{align-self:flex-end}.p-modal__close{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='90' width='90'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h90v90H0z'/%3E%3Cpath d='M14.52 6L6 14.52 36.48 45 6 75.49 14.52 84 45 53.52 75.48 84 84 75.49 53.52 45 84 14.52 75.48 6 45 36.49z' fill='%23888'/%3E%3C/g%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;background-size:1rem;border:0;box-sizing:content-box;height:1rem;margin:0;padding:.5rem .5rem;position:absolute;right:0;text-indent:-999em;top:0;width:1rem}.p-modal__close:focus{outline:.1875rem solid #2e96ff}.p-navigation__row,.p-navigation__row--full-width,.p-navigation.row{display:flex;flex-direction:column}@media (min-width: 772px){.p-navigation__row,.p-navigation__row--full-width,.p-navigation.row{flex-direction:row}}.p-navigation__item .p-navigation__link,.p-navigation__link>a,.p-navigation__toggle--open,.p-navigation__toggle--close{padding-bottom:.75rem;padding-top:.75rem}@media (max-width: 620px){.p-navigation__item .p-navigation__link,.p-navigation__link>a,.p-navigation__toggle--open,.p-navigation__toggle--close,.p-subnav__item,.p-navigation__banner{padding-left:1rem}}@media (min-width: 620px) and (max-width: 772px){.p-navigation__item .p-navigation__link,.p-navigation__link>a,.p-navigation__toggle--open,.p-navigation__toggle--close,.p-subnav__item,.p-navigation__banner{padding-left:1.5rem}}@media (min-width: 772px){.p-navigation__item .p-navigation__link,.p-navigation__link>a,.p-navigation__toggle--open,.p-navigation__toggle--close,.p-subnav__item,.p-navigation__banner{padding-left:1rem}}@media (max-width: 620px){.p-navigation__item .p-navigation__link,.p-navigation__link>a,.p-navigation__toggle--open,.p-navigation__toggle--close,.p-subnav__item{padding-right:1rem}}@media (min-width: 620px) and (max-width: 772px){.p-navigation__item .p-navigation__link,.p-navigation__link>a,.p-navigation__toggle--open,.p-navigation__toggle--close,.p-subnav__item{padding-right:1.5rem}}@media (min-width: 772px){.p-navigation__item .p-navigation__link,.p-navigation__link>a,.p-navigation__toggle--open,.p-navigation__toggle--close,.p-subnav__item{padding-right:1rem}}.p-navigation__item .p-navigation__link,.p-navigation__link>a{transition-duration:0.1s;transition-property:background-color,color,opacity;transition-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1);display:block;line-height:1.5rem;margin-bottom:0;overflow:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap}.p-navigation__item .p-navigation__link:focus,.p-navigation__link>a:focus{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-navigation__item .p-navigation__link:focus-visible,.p-navigation__link>a:focus-visible{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-navigation__item .p-navigation__link:focus:not(:focus-visible),.p-navigation__link>a:focus:not(:focus-visible){outline:0;outline-offset:0}.p-navigation__item .p-navigation__link::before,.p-navigation__link>a::before{content:'';height:1px;left:0;position:absolute;right:0;top:0}@media (min-width: 772px){.p-navigation__item .p-navigation__link::before,.p-navigation__link>a::before{content:none}}.p-navigation__item .p-navigation__link,.p-navigation__link>a,.p-navigation__item .p-navigation__link:visited,.p-navigation__link>a:visited,.p-navigation__item .p-navigation__link:focus,.p-navigation__link>a:focus,.p-navigation__item .p-navigation__link:hover,.p-navigation__link>a:hover{text-decoration:none}.p-navigation__items,.p-navigation__links{list-style:none;margin:-1px 0 0 0;padding:0}@media (min-width: 772px){.p-navigation__items,.p-navigation__links{display:flex;flex-wrap:wrap;margin-top:0}}@media (min-width: 772px){.p-navigation__item,.p-navigation__link{max-width:20em}}.p-navigation__logo .p-navigation__item,.p-navigation__logo .p-navigation__link{display:flex}.p-navigation__logo .p-navigation__item:focus,.p-navigation__logo .p-navigation__link:focus{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-navigation__logo .p-navigation__item:focus-visible,.p-navigation__logo .p-navigation__link:focus-visible{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-navigation__logo .p-navigation__item:focus:not(:focus-visible),.p-navigation__logo .p-navigation__link:focus:not(:focus-visible){outline:0;outline-offset:0}.p-navigation__row{padding-left:0;padding-right:0}.p-navigation{display:flex;flex-direction:column;flex-shrink:0;position:relative;z-index:10}@media (min-width: 772px){.p-navigation{flex-direction:row}}.p-navigation__row--full-width{width:100%}.p-navigation__banner{display:flex;flex:0 0 auto;justify-content:space-between}@media (max-width: 620px){.p-navigation__banner{padding-right:0}}@media (min-width: 620px) and (max-width: 772px){.p-navigation__banner{padding-right:0}}@media (min-width: 772px){.p-navigation__banner{padding-left:1.5rem}}.p-navigation__logo{display:flex;flex:0 0 auto;height:3rem;margin:0 1rem 0 0}.p-navigation__image{align-self:center;max-height:2rem;min-height:1.5rem}.p-navigation__nav{display:none;flex-direction:column}@media (min-width: 772px){.p-navigation__nav{display:flex;flex-direction:row;justify-content:space-between;margin-right:1.5rem;width:100%}}.p-navigation .p-search-box{flex:1 0 auto;margin-top:-1px;min-width:10em;order:-1}@media (max-width: 620px){.p-navigation .p-search-box{margin-left:1rem;margin-right:1rem}}@media (min-width: 620px) and (max-width: 772px){.p-navigation .p-search-box{margin-left:1.5rem;margin-right:1.5rem}}@media (min-width: 772px){.p-navigation .p-search-box{display:flex;flex:1 1 auto;margin:.35rem 0 auto auto;max-width:20rem;min-width:initial;order:1}}.p-navigation__toggle--close{display:none}.p-navigation__toggle--open{display:block}@media (min-width: 772px){.p-navigation__toggle--open{display:none}}.p-navigation__toggle--open,.p-navigation__toggle--close{margin:0 0 auto 0}.p-navigation__toggle--open:focus,.p-navigation__toggle--close:focus{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-navigation__toggle--open:focus-visible,.p-navigation__toggle--close:focus-visible{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-navigation__toggle--open:focus:not(:focus-visible),.p-navigation__toggle--close:focus:not(:focus-visible){outline:0;outline-offset:0}.p-navigation__toggle--open,.p-navigation__toggle--open:visited,.p-navigation__toggle--open:focus,.p-navigation__toggle--open:hover,.p-navigation__toggle--close,.p-navigation__toggle--close:visited,.p-navigation__toggle--close:focus,.p-navigation__toggle--close:hover{text-decoration:none}.p-navigation:target::after{display:none}.p-navigation:target .p-navigation__nav{display:flex}.p-navigation:target .p-navigation__toggle--open{display:none}.p-navigation:target .p-navigation__toggle--close{display:block}@media (min-width: 772px){.p-navigation:target .p-navigation__toggle--close{display:none}}.p-navigation .u-image-position .u-image-position--right{order:2;position:relative;right:initial}.p-navigation{background-color:#fff}.p-navigation .p-navigation__item>.p-navigation__link,.p-navigation .p-navigation__toggle--close,.p-navigation .p-navigation__toggle--open,.p-navigation .p-navigation__link>a,.p-navigation.is-dark .p-navigation__item>.p-navigation__link,.p-navigation.is-dark .p-navigation__toggle--close,.p-navigation.is-dark .p-navigation__toggle--open,.p-navigation.is-dark .p-navigation__link>a,.p-navigation .p-navigation__item>.p-navigation__link:visited,.p-navigation .p-navigation__toggle--close:visited,.p-navigation .p-navigation__toggle--open:visited,.p-navigation .p-navigation__link>a:visited,.p-navigation .p-navigation__item>.p-navigation__link:focus,.p-navigation .p-navigation__toggle--close:focus,.p-navigation .p-navigation__toggle--open:focus,.p-navigation .p-navigation__link>a:focus{color:#111}.p-navigation .p-navigation__item>.p-navigation__link:hover,.p-navigation .p-navigation__toggle--close:hover,.p-navigation .p-navigation__toggle--open:hover,.p-navigation .p-navigation__link>a:hover,.p-navigation .p-navigation__item.is-selected>.p-navigation__link,.p-navigation .p-navigation__link.is-selected>a,.p-navigation.is-dark .p-navigation__item.is-selected>.p-navigation__link,.p-navigation.is-dark .p-navigation__link.is-selected>a{background-color:#f7f7f7;color:#111}.p-navigation .p-navigation__item>.p-navigation__link::before,.p-navigation .p-navigation__link>a::before,.p-navigation.is-dark .p-navigation__item>.p-navigation__link::before,.p-navigation.is-dark .p-navigation__link>a::before{background:rgba(0,0,0,0.2)}.p-navigation.is-dark{background-color:#333}.p-navigation.is-dark .p-navigation__item>.p-navigation__link,.p-navigation.is-dark .p-navigation__toggle--close,.p-navigation.is-dark .p-navigation__toggle--open,.p-navigation.is-dark .p-navigation__link>a,.p-navigation.is-dark .p-navigation__item>.p-navigation__link:visited,.p-navigation.is-dark .p-navigation__toggle--close:visited,.p-navigation.is-dark .p-navigation__toggle--open:visited,.p-navigation.is-dark .p-navigation__link>a:visited,.p-navigation.is-dark .p-navigation__item>.p-navigation__link:focus,.p-navigation.is-dark .p-navigation__toggle--close:focus,.p-navigation.is-dark .p-navigation__toggle--open:focus,.p-navigation.is-dark .p-navigation__link>a:focus{color:#fff}.p-navigation.is-dark .p-navigation__item>.p-navigation__link:hover,.p-navigation.is-dark .p-navigation__toggle--close:hover,.p-navigation.is-dark .p-navigation__toggle--open:hover,.p-navigation.is-dark .p-navigation__link>a:hover,.p-navigation.is-dark .p-navigation__item.is-selected>.p-navigation__link,.p-navigation.is-dark .p-navigation__link.is-selected>a{background-color:#fff;color:#262626}.p-navigation.is-dark .p-navigation__item>.p-navigation__link::before,.p-navigation.is-dark .p-navigation__link>a::before{background:rgba(255,255,255,0.2)}.p-subnav{position:relative}.p-subnav::after{background-position:center;background-repeat:no-repeat;background-size:contain;content:'';display:block;height:1rem;pointer-events:none;position:absolute;text-indent:calc(100% + 10rem);top:1rem;width:1rem}@media (max-width: 620px){.p-subnav::after{right:1rem}}@media (min-width: 620px) and (max-width: 772px){.p-subnav::after{right:1.5rem}}@media (min-width: 773px){.p-subnav::after{right:calc(.5rem + 1px)}}.p-subnav__items,.p-subnav__items--right{display:none;margin:0;min-width:100%;padding:0;z-index:5}@media (min-width: 773px){.p-subnav__items,.p-subnav__items--right{position:absolute;top:3rem}}@media (max-width: 772px){.p-subnav__items,.p-subnav__items--right{box-shadow:none}}.p-subnav__items--right{right:0}.p-subnav.is-active::after{transform:rotate(180deg)}.p-subnav.is-active .p-subnav__items,.p-subnav.is-active .p-subnav__items--right{display:block}.p-subnav>.p-navigation__link,.p-subnav>a{padding-right:2rem}.p-subnav__item{display:block;white-space:nowrap}@media (max-width: 772px){.p-subnav__item{padding-bottom:.5rem;padding-top:.5rem}}@media (min-width: 772px){.p-subnav__item{padding-bottom:.75rem;padding-top:.75rem}}.p-subnav__item,.p-subnav__item:active,.p-subnav__item:focus,.p-subnav__item:hover,.p-subnav__item:visited{text-decoration:none}.p-subnav::after{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23666' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E")}.p-subnav .p-subnav__item{background-color:#fff}.p-subnav .p-subnav__item,.p-subnav .p-subnav__item:active,.p-subnav .p-subnav__item:focus,.p-subnav .p-subnav__item:visited{color:#111}.p-subnav .p-subnav__item:hover{background-color:#f7f7f7;color:#111}.p-subnav.is-dark::after,.p-navigation.is-dark .p-subnav::after{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23999' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E")}.p-subnav.is-dark .p-subnav__item,.p-navigation.is-dark .p-subnav .p-subnav__item{background-color:#333}.p-subnav.is-dark .p-subnav__item,.p-subnav.is-dark .p-subnav__item:active,.p-subnav.is-dark .p-subnav__item:focus,.p-subnav.is-dark .p-subnav__item:visited,.p-navigation.is-dark .p-subnav .p-subnav__item,.p-navigation.is-dark .p-subnav .p-subnav__item:active,.p-navigation.is-dark .p-subnav .p-subnav__item:focus,.p-navigation.is-dark .p-subnav .p-subnav__item:visited{color:#fff}.p-subnav.is-dark .p-subnav__item:hover,.p-navigation.is-dark .p-subnav .p-subnav__item:hover{background-color:#fff;color:#262626}.p-notification,.p-notification--positive,.p-notification--caution,.p-notification--negative,.p-notification--information{display:flex;overflow:hidden;padding:0}.p-notification .p-icon--close,.p-notification--positive .p-icon--close,.p-notification--caution .p-icon--close,.p-notification--negative .p-icon--close,.p-notification--information .p-icon--close{background-color:transparent;background-size:1rem;border:0;margin:.9375rem 1rem auto auto;padding:.5rem}.p-notification{position:relative}.p-notification::before{top:0;background-color:#666;content:'';position:absolute}.p-notification::before{height:.1875rem;width:auto;left:0;right:0}.p-notification+.p-notification{margin-top:1.5rem}.p-notification__response{background-position:1rem .9375rem;background-repeat:no-repeat;background-size:1rem;padding:.6875rem 1rem .5rem}.p-notification__status::after,.p-notification__action::before{content:' '}.p-notification__response,.p-notification--floating{max-width:unset}.p-notification--positive{position:relative}.p-notification--positive::before{top:0;background-color:#0e8620;content:'';position:absolute}.p-notification--positive::before{height:.1875rem;width:auto;left:0;right:0}.p-notification--positive .p-notification__response{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cg fill='none' fill-rule='nonzero'%3E%3Cpath fill='%230e8620' d='M8 1a7 7 0 110 14A7 7 0 018 1zm2.83 3.502L6.863 9.884 5.174 8.096l-1.09 1.03 2.92 3.096 5.034-6.83-1.208-.89z'/%3E%3Cpath fill='%23fff' d='M10.83 4.502l1.208.89-5.033 6.83-2.922-3.096 1.091-1.03 1.689 1.789z'/%3E%3C/g%3E%3C/svg%3E");padding-left:3rem}.p-notification--caution{position:relative}.p-notification--caution::before{top:0;background-color:#f99b11;content:'';position:absolute}.p-notification--caution::before{height:.1875rem;width:auto;left:0;right:0}.p-notification--caution .p-notification__response{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M9.34 1.2l5.842 11.627A1.5 1.5 0 0113.842 15H2.158a1.5 1.5 0 01-1.34-2.173L6.66 1.2a1.5 1.5 0 012.68 0z' fill='%23f99b11'/%3E%3Cpath d='M8.5 11a.5.5 0 01.492.41L9 11.5v1a.5.5 0 01-.41.492L8.5 13h-1a.5.5 0 01-.492-.41L7 12.5v-1a.5.5 0 01.41-.492L7.5 11h1zM9 5v4.5H7V5h2z' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E");padding-left:3rem}.p-notification--negative{position:relative}.p-notification--negative::before{top:0;background-color:#c7162b;content:'';position:absolute}.p-notification--negative::before{height:.1875rem;width:auto;left:0;right:0}.p-notification--negative .p-notification__response{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle stroke='%23C7162B' stroke-width='1.5' fill='%23c7162b' cx='8' cy='8' r='6.25'/%3E%3Cpath fill='%23FFF' fill-rule='nonzero' d='M10.282 4.638l1.06 1.06L9.05 7.99l2.293 2.292-1.06 1.06L7.99 9.05 5.7 11.343l-1.06-1.06 2.29-2.293L4.64 5.7l1.06-1.06 2.291 2.29z'/%3E%3C/g%3E%3C/svg%3E");padding-left:3rem}.p-notification--information{position:relative}.p-notification--information::before{top:0;background-color:#24598f;content:'';position:absolute}.p-notification--information::before{height:.1875rem;width:auto;left:0;right:0}.p-pagination__link,.p-pagination__link--previous,.p-pagination__link--next{background-color:#fff;border-color:rgba(0,0,0,0.56);color:#111}.p-pagination__link:visited,.p-pagination__link--previous:visited,.p-pagination__link--next:visited{color:#111}.p-pagination__link:hover,.p-pagination__link--previous:hover,.p-pagination__link--next:hover{background-color:#f2f2f2;border-color:rgba(0,0,0,0.56)}.p-pagination__link:active,.p-pagination__link--previous:active,.p-pagination__link--next:active{background-color:#d9d9d9;border-color:rgba(0,0,0,0.56);transition-duration:0s}.p-pagination__link:disabled:active,.p-pagination__link--previous:disabled:active,.p-pagination__link--next:disabled:active,.p-pagination__link:disabled:hover,.p-pagination__link--previous:disabled:hover,.p-pagination__link--next:disabled:hover,.is-disabled.p-pagination__link:active,.is-disabled.p-pagination__link--previous:active,.is-disabled.p-pagination__link--next:active,.is-disabled.p-pagination__link:hover,.is-disabled.p-pagination__link--previous:hover,.is-disabled.p-pagination__link--next:hover{background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0.56)}.p-pagination__link .p-link--external,.p-pagination__link--previous .p-link--external,.p-pagination__link--next .p-link--external{color:currentColor}.is-active.p-pagination__link,.is-active.p-pagination__link--previous,.is-active.p-pagination__link--next{background-color:#d9d9d9;color:#111;text-decoration:none}.p-pagination{display:flex;flex-direction:row;list-style:none;margin-bottom:0;margin-left:0;padding-left:0}.p-pagination__item{width:auto}.p-pagination__item+.p-pagination__item:not(:nth-child(2)):not(:nth-last-child(1)){margin-left:.5rem}.p-pagination__item+.p-pagination__item:nth-child(2),.p-pagination__item+.p-pagination__item:nth-last-child(1){margin-left:1rem}.p-pagination__item--truncation{padding:calc(.4rem - 1px) 0}.p-pagination__link--next [class*='p-icon'],.p-pagination__link--previous [class*='p-icon']{margin-left:.5rem;margin-right:.5rem}.p-pagination__link--next [class*='p-icon']:first-child,.p-pagination__link--previous [class*='p-icon']:first-child{margin-left:-.5rem}.p-pagination__link--next [class*='p-icon']:last-child,.p-pagination__link--previous [class*='p-icon']:last-child{margin-right:-.5rem}.p-pagination__link--previous [class*='p-icon']{transform:rotate(0.25turn)}.p-pagination__link--next [class*='p-icon']{transform:rotate(-0.25turn)}.p-pull-quote,.p-pull-quote--small,.p-pull-quote--large{border:0;margin:1.5rem 0 2rem;overflow:visible;position:relative}.p-pull-quote.has-image,.p-pull-quote--small.has-image,.p-pull-quote--large.has-image{margin-top:calc(2.75rem + 1.5rem)}.p-pull-quote .p-pull-quote__citation,.p-pull-quote--small .p-pull-quote__citation,.p-pull-quote--large .p-pull-quote__citation{margin-top:.5rem}.p-pull-quote__image{height:2rem;position:absolute;top:-2.75rem}.p-pull-quote__quote{position:relative}.p-pull-quote__quote:first-of-type::before{color:#666;display:inline-block;position:absolute;width:0.5em;content:'\201C';left:-0.75em;text-align:right;top:0.5rem}.p-pull-quote__quote:last-of-type{margin-bottom:0}.p-pull-quote__quote:last-of-type::after{color:#666;display:inline-block;position:absolute;width:0.5em;bottom:0.55em;content:'\201E';margin-left:0.25em}.p-pull-quote{padding:0 2rem}.p-pull-quote .p-pull-quote__quote::before,.p-pull-quote .p-pull-quote__quote::after{font-size:2em}.p-pull-quote--small{padding:0 1.5rem}.p-pull-quote--small .p-pull-quote__quote::before,.p-pull-quote--small .p-pull-quote__quote::after{font-size:1.5em}.p-pull-quote--large{padding:0 2.5rem}.p-pull-quote--large .p-pull-quote__quote::before,.p-pull-quote--large .p-pull-quote__quote::after{font-size:2em;max-width:1em}@media (max-width: 772px){.p-pull-quote .p-pull-quote__quote:first-of-type::before,.p-pull-quote--large .p-pull-quote__quote:first-of-type::before{top:0.75rem}}.p-search-box__reset,.p-search-box__button{display:block;height:calc(2.3rem - .375rem);margin:.1875rem 0;position:relative;width:auto}.p-search-box__reset:hover,.p-search-box__button:hover{background:inherit}.p-search-box__reset:hover:disabled,.p-search-box__button:hover:disabled{cursor:not-allowed}.p-search-box__reset [class*='p-icon'],.p-search-box__button [class*='p-icon']{vertical-align:0}.p-search-box__reset [class*='p-icon']:only-child,.p-search-box__button [class*='p-icon']:only-child{margin-left:-.5rem;margin-right:-.5rem}.p-search-box{display:flex;justify-content:flex-end;margin-bottom:1.2rem;position:relative}.p-search-box__reset:not(:last-of-type):not(:only-of-type){margin-right:.1875rem}.p-search-box__input{flex:1 1 100%;margin-bottom:0;padding-right:calc(2 * 2.3rem);position:absolute;right:0}.p-search-box__input::-webkit-search-cancel-button{-webkit-appearance:none}.p-search-box__input:not(:valid) ~ .p-search-box__reset{display:none}.p-search-box__button{border-left-style:solid;border-left-width:1px;border-radius:0 .125rem .125rem 0;margin-right:.1875rem}.p-search-box__reset,.p-search-box__button,.p-search-box__reset:active,.p-search-box__button:active,.p-search-box__reset:focus,.p-search-box__button:focus,.p-search-box__reset:hover,.p-search-box__button:hover{background-color:transparent !important;border-width:0}.p-search-box .p-search-box__input{background-color:#fff;border-color:rgba(0,0,0,0.56);color:#111}.p-search-box .p-search-box__input:active,.p-search-box .p-search-box__input:focus,.p-search-box .p-search-box__input:hover,.p-search-box .p-search-box__input:-internal-autofill-selected,.p-search-box .p-search-box__input:-webkit-autofill,.p-search-box .p-search-box__input:-webkit-autofill:hover,.p-search-box .p-search-box__input:-webkit-autofill:focus{background-color:#fff !important;border-color:rgba(0,0,0,0.56) !important}.p-search-box.is-dark .p-search-box__input{background-color:#262626;border-color:rgba(255,255,255,0.4);color:#fff}.p-search-box.is-dark .p-search-box__input:active,.p-search-box.is-dark .p-search-box__input:focus,.p-search-box.is-dark .p-search-box__input:hover,.p-search-box.is-dark .p-search-box__input:-internal-autofill-selected,.p-search-box.is-dark .p-search-box__input:-webkit-autofill,.p-search-box.is-dark .p-search-box__input:-webkit-autofill:hover,.p-search-box.is-dark .p-search-box__input:-webkit-autofill:focus{background-color:#262626 !important;border-color:rgba(255,255,255,0.4) !important}@media only screen and (max-width: 1036px){.p-separator{margin-bottom:2rem;margin-top:2rem}}@media only screen and (min-width: 1036px){.p-separator{margin-bottom:4rem;margin-top:4rem}}@-webkit-keyframes vf-p-side-navigation-expand{0%{transform:translate(-100%)}100%{transform:translate(0)}}@keyframes vf-p-side-navigation-expand{0%{transform:translate(-100%)}100%{transform:translate(0)}}@-webkit-keyframes vf-p-side-navigation-collapse{0%{transform:translate(0)}100%{transform:translate(-100%)}}@keyframes vf-p-side-navigation-collapse{0%{transform:translate(0)}100%{transform:translate(-100%)}}.p-side-navigation__drawer{bottom:0;left:0;overflow:auto;position:fixed;top:0;transform:translateX(-100%);width:100%;z-index:102}.p-side-navigation:target .p-side-navigation__drawer,[class*='p-side-navigation--']:target .p-side-navigation__drawer,.p-side-navigation.is-expanded .p-side-navigation__drawer,[class*='p-side-navigation--'].is-expanded .p-side-navigation__drawer{-webkit-animation:vf-p-side-navigation-expand 0.333s;animation:vf-p-side-navigation-expand 0.333s;transform:translateX(0)}.p-side-navigation.is-collapsed .p-side-navigation__drawer,[class*='p-side-navigation--'].is-collapsed .p-side-navigation__drawer{-webkit-animation:vf-p-side-navigation-collapse 0.333s;animation:vf-p-side-navigation-collapse 0.333s}@media (min-width: 460px){.p-side-navigation__drawer{max-width:20rem}}.p-side-navigation__overlay{transition-duration:0.333s;transition-property:opacity;transition-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1);bottom:0;left:0;opacity:0;pointer-events:none;position:fixed;right:0;top:0;z-index:101}.p-side-navigation:target .p-side-navigation__overlay,[class*='p-side-navigation--']:target .p-side-navigation__overlay,.p-side-navigation.is-expanded .p-side-navigation__overlay,[class*='p-side-navigation--'].is-expanded .p-side-navigation__overlay{opacity:1;pointer-events:all}.p-side-navigation__drawer-header{border-bottom-style:solid;border-bottom-width:1px;padding-bottom:.5rem;padding-left:.5rem;padding-top:.5rem;position:-webkit-sticky;position:sticky;top:0;z-index:1}.p-side-navigation__toggle,.p-side-navigation__toggle--in-drawer{width:auto}.p-side-navigation__toggle::before,.p-side-navigation__toggle--in-drawer::before{content:'';margin-left:-.5rem;margin-right:.5rem}.p-side-navigation__toggle::before{transform:rotate(-90deg)}.p-side-navigation__toggle--in-drawer{margin-bottom:0}.p-side-navigation__toggle--in-drawer::before{transform:rotate(90deg)}@media (min-width: 772px){.p-side-navigation.is-sticky,[class*='p-side-navigation--'].is-sticky{max-height:100vh;overflow-y:auto;position:-webkit-sticky;position:sticky;top:0}.p-side-navigation__toggle,.p-side-navigation__toggle--in-drawer,.p-side-navigation__drawer-header{display:none}.p-side-navigation__drawer,.p-side-navigation:target .p-side-navigation__drawer,[class*='p-side-navigation--']:target .p-side-navigation__drawer,.p-side-navigation.is-expanded .p-side-navigation__drawer,[class*='p-side-navigation--'].is-expanded .p-side-navigation__drawer{box-shadow:none;display:block;max-width:none;overflow:visible;position:static;transform:translateX(0)}.p-side-navigation__overlay{display:none}}.p-side-navigation__list::after,.p-side-navigation--raw-html ul::after{bottom:-0.75rem}@media (max-width: 620px){.p-side-navigation__list::after,.p-side-navigation--raw-html ul::after{left:1rem}}@media (min-width: 620px) and (max-width: 772px){.p-side-navigation__list::after,.p-side-navigation--raw-html ul::after{left:1.5rem}}@media (min-width: 772px){.p-side-navigation__list::after,.p-side-navigation--raw-html ul::after{left:1.5rem}}@media (max-width: 620px){.p-side-navigation--icons .p-side-navigation__list::after,.p-side-navigation--icons .p-side-navigation--raw-html ul::after,.p-side-navigation--raw-html .p-side-navigation--icons ul::after{left:3rem}}@media (min-width: 620px) and (max-width: 772px){.p-side-navigation--icons .p-side-navigation__list::after,.p-side-navigation--icons .p-side-navigation--raw-html ul::after,.p-side-navigation--raw-html .p-side-navigation--icons ul::after{left:4rem}}@media (min-width: 772px){.p-side-navigation--icons .p-side-navigation__list::after,.p-side-navigation--icons .p-side-navigation--raw-html ul::after,.p-side-navigation--raw-html .p-side-navigation--icons ul::after{left:4rem}}.p-side-navigation__list:last-of-type::after,.p-side-navigation--raw-html ul:last-of-type::after{content:none}.p-side-navigation>.p-side-navigation__list:first-of-type,.p-side-navigation__drawer>.p-side-navigation__list:first-of-type{padding-top:0.125rem}.p-side-navigation__item--title{font-weight:400}.p-side-navigation__text,.p-side-navigation__link,.p-side-navigation--raw-html h2,.p-side-navigation--raw-html h3,.p-side-navigation--raw-html h4,.p-side-navigation--raw-html h5,.p-side-navigation--raw-html h6,.p-side-navigation--raw-html li>span,.p-side-navigation--raw-html li>strong,.p-side-navigation--raw-html li>a{display:flex;padding-bottom:.25rem;padding-right:1rem;padding-top:.25rem}@media (max-width: 620px){.p-side-navigation__text,.p-side-navigation__link,.p-side-navigation--raw-html h2,.p-side-navigation--raw-html h3,.p-side-navigation--raw-html h4,.p-side-navigation--raw-html h5,.p-side-navigation--raw-html h6,.p-side-navigation--raw-html li>span,.p-side-navigation--raw-html li>strong,.p-side-navigation--raw-html li>a{padding-left:1rem}}@media (min-width: 620px) and (max-width: 772px){.p-side-navigation__text,.p-side-navigation__link,.p-side-navigation--raw-html h2,.p-side-navigation--raw-html h3,.p-side-navigation--raw-html h4,.p-side-navigation--raw-html h5,.p-side-navigation--raw-html h6,.p-side-navigation--raw-html li>span,.p-side-navigation--raw-html li>strong,.p-side-navigation--raw-html li>a{padding-left:1.5rem}}@media (min-width: 772px){.p-side-navigation__text,.p-side-navigation__link,.p-side-navigation--raw-html h2,.p-side-navigation--raw-html h3,.p-side-navigation--raw-html h4,.p-side-navigation--raw-html h5,.p-side-navigation--raw-html h6,.p-side-navigation--raw-html li>span,.p-side-navigation--raw-html li>strong,.p-side-navigation--raw-html li>a{padding-left:1.5rem}}.p-side-navigation__link:focus,.p-side-navigation--raw-html li>a:focus{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-side-navigation__link:focus-visible,.p-side-navigation--raw-html li>a:focus-visible{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-side-navigation__link:focus:not(:focus-visible),.p-side-navigation--raw-html li>a:focus:not(:focus-visible){outline:0;outline-offset:0}.p-side-navigation__link:focus::before,.p-side-navigation--raw-html li>a:focus::before{display:none}.p-side-navigation__link:hover,.p-side-navigation--raw-html li>a:hover{text-decoration:none}.p-side-navigation--icons .p-side-navigation__text,.p-side-navigation--icons .p-side-navigation__link{position:relative}@media (max-width: 620px){.p-side-navigation--icons .p-side-navigation__text,.p-side-navigation--icons .p-side-navigation__link{padding-left:3rem}}@media (min-width: 620px) and (max-width: 772px){.p-side-navigation--icons .p-side-navigation__text,.p-side-navigation--icons .p-side-navigation__link{padding-left:4rem}}@media (min-width: 772px){.p-side-navigation--icons .p-side-navigation__text,.p-side-navigation--icons .p-side-navigation__link{padding-left:4rem}}@media (max-width: 620px){.p-side-navigation__item .p-side-navigation__item .p-side-navigation__text,.p-side-navigation__item .p-side-navigation__item .p-side-navigation__link{padding-left:2rem}}@media (min-width: 620px) and (max-width: 772px){.p-side-navigation__item .p-side-navigation__item .p-side-navigation__text,.p-side-navigation__item .p-side-navigation__item .p-side-navigation__link{padding-left:3rem}}@media (min-width: 772px){.p-side-navigation__item .p-side-navigation__item .p-side-navigation__text,.p-side-navigation__item .p-side-navigation__item .p-side-navigation__link{padding-left:3rem}}@media (max-width: 620px){.p-side-navigation--icons .p-side-navigation__item .p-side-navigation__item .p-side-navigation__text,.p-side-navigation--icons .p-side-navigation__item .p-side-navigation__item .p-side-navigation__link{padding-left:4rem}}@media (min-width: 620px) and (max-width: 772px){.p-side-navigation--icons .p-side-navigation__item .p-side-navigation__item .p-side-navigation__text,.p-side-navigation--icons .p-side-navigation__item .p-side-navigation__item .p-side-navigation__link{padding-left:5.5rem}}@media (min-width: 772px){.p-side-navigation--icons .p-side-navigation__item .p-side-navigation__item .p-side-navigation__text,.p-side-navigation--icons .p-side-navigation__item .p-side-navigation__item .p-side-navigation__link{padding-left:5.5rem}}@media (max-width: 620px){.p-side-navigation__item .p-side-navigation__item .p-side-navigation__item .p-side-navigation__text,.p-side-navigation__item .p-side-navigation__item .p-side-navigation__item .p-side-navigation__link{padding-left:3rem}}@media (min-width: 620px) and (max-width: 772px){.p-side-navigation__item .p-side-navigation__item .p-side-navigation__item .p-side-navigation__text,.p-side-navigation__item .p-side-navigation__item .p-side-navigation__item .p-side-navigation__link{padding-left:4.5rem}}@media (min-width: 772px){.p-side-navigation__item .p-side-navigation__item .p-side-navigation__item .p-side-navigation__text,.p-side-navigation__item .p-side-navigation__item .p-side-navigation__item .p-side-navigation__link{padding-left:4.5rem}}@media (max-width: 620px){.p-side-navigation--icons .p-side-navigation__item .p-side-navigation__item .p-side-navigation__item .p-side-navigation__text,.p-side-navigation--icons .p-side-navigation__item .p-side-navigation__item .p-side-navigation__item .p-side-navigation__link{padding-left:5rem}}@media (min-width: 620px) and (max-width: 772px){.p-side-navigation--icons .p-side-navigation__item .p-side-navigation__item .p-side-navigation__item .p-side-navigation__text,.p-side-navigation--icons .p-side-navigation__item .p-side-navigation__item .p-side-navigation__item .p-side-navigation__link{padding-left:7rem}}@media (min-width: 772px){.p-side-navigation--icons .p-side-navigation__item .p-side-navigation__item .p-side-navigation__item .p-side-navigation__text,.p-side-navigation--icons .p-side-navigation__item .p-side-navigation__item .p-side-navigation__item .p-side-navigation__link{padding-left:7rem}}.p-side-navigation--icons .p-side-navigation__icon{position:absolute;top:.5rem}@media (max-width: 620px){.p-side-navigation--icons .p-side-navigation__icon{left:1rem}}@media (min-width: 620px) and (max-width: 772px){.p-side-navigation--icons .p-side-navigation__icon{left:1.5rem}}@media (min-width: 772px){.p-side-navigation--icons .p-side-navigation__icon{left:1.5rem}}.p-side-navigation__status{margin-left:auto;padding-left:.5rem}.p-side-navigation--raw-html h2,.p-side-navigation--raw-html h3,.p-side-navigation--raw-html h4,.p-side-navigation--raw-html h5,.p-side-navigation--raw-html h6{font-size:1rem;margin:0}@media (max-width: 620px){.p-side-navigation--raw-html li li>span,.p-side-navigation--raw-html li li>strong,.p-side-navigation--raw-html li li>a{padding-left:2rem}}@media (min-width: 620px) and (max-width: 772px){.p-side-navigation--raw-html li li>span,.p-side-navigation--raw-html li li>strong,.p-side-navigation--raw-html li li>a{padding-left:3rem}}@media (min-width: 772px){.p-side-navigation--raw-html li li>span,.p-side-navigation--raw-html li li>strong,.p-side-navigation--raw-html li li>a{padding-left:3rem}}@media (max-width: 620px){.p-side-navigation--raw-html li li li>span,.p-side-navigation--raw-html li li li>strong,.p-side-navigation--raw-html li li li>a{padding-left:3rem}}@media (min-width: 620px) and (max-width: 772px){.p-side-navigation--raw-html li li li>span,.p-side-navigation--raw-html li li li>strong,.p-side-navigation--raw-html li li li>a{padding-left:4.5rem}}@media (min-width: 772px){.p-side-navigation--raw-html li li li>span,.p-side-navigation--raw-html li li li>strong,.p-side-navigation--raw-html li li li>a{padding-left:4.5rem}}.p-side-navigation,[class*='p-side-navigation--']{color:#666}.p-side-navigation .p-side-navigation__toggle,[class*='p-side-navigation--'] .p-side-navigation__toggle{background-color:#fff;border-color:rgba(0,0,0,0.56);color:#111}.p-side-navigation .p-side-navigation__toggle:visited,[class*='p-side-navigation--'] .p-side-navigation__toggle:visited{color:#111}.p-side-navigation .p-side-navigation__toggle:hover,[class*='p-side-navigation--'] .p-side-navigation__toggle:hover{background-color:#f2f2f2;border-color:rgba(0,0,0,0.56)}.p-side-navigation .p-side-navigation__toggle:active,[class*='p-side-navigation--'] .p-side-navigation__toggle:active{background-color:#d9d9d9;border-color:rgba(0,0,0,0.56);transition-duration:0s}.p-side-navigation .p-side-navigation__toggle:disabled:active,.p-side-navigation .p-side-navigation__toggle:disabled:hover,.p-side-navigation .p-side-navigation__toggle.is-disabled:active,.p-side-navigation .p-side-navigation__toggle.is-disabled:hover,[class*='p-side-navigation--'] .p-side-navigation__toggle:disabled:active,[class*='p-side-navigation--'] .p-side-navigation__toggle:disabled:hover,[class*='p-side-navigation--'] .p-side-navigation__toggle.is-disabled:active,[class*='p-side-navigation--'] .p-side-navigation__toggle.is-disabled:hover{background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0.56)}.p-side-navigation .p-side-navigation__toggle .p-link--external,[class*='p-side-navigation--'] .p-side-navigation__toggle .p-link--external{color:currentColor}.p-side-navigation .p-side-navigation__toggle::before,[class*='p-side-navigation--'] .p-side-navigation__toggle::before{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23666' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E")}.p-side-navigation .p-side-navigation__toggle--in-drawer,[class*='p-side-navigation--'] .p-side-navigation__toggle--in-drawer{background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0);color:#666}.p-side-navigation .p-side-navigation__toggle--in-drawer:visited,[class*='p-side-navigation--'] .p-side-navigation__toggle--in-drawer:visited{color:#666}.p-side-navigation .p-side-navigation__toggle--in-drawer:hover,[class*='p-side-navigation--'] .p-side-navigation__toggle--in-drawer:hover{background-color:rgba(0,0,0,0.05);border-color:rgba(0,0,0,0)}.p-side-navigation .p-side-navigation__toggle--in-drawer:active,[class*='p-side-navigation--'] .p-side-navigation__toggle--in-drawer:active{background-color:rgba(0,0,0,0.15);border-color:rgba(0,0,0,0);transition-duration:0s}.p-side-navigation .p-side-navigation__toggle--in-drawer:disabled:active,.p-side-navigation .p-side-navigation__toggle--in-drawer:disabled:hover,.p-side-navigation .p-side-navigation__toggle--in-drawer.is-disabled:active,.p-side-navigation .p-side-navigation__toggle--in-drawer.is-disabled:hover,[class*='p-side-navigation--'] .p-side-navigation__toggle--in-drawer:disabled:active,[class*='p-side-navigation--'] .p-side-navigation__toggle--in-drawer:disabled:hover,[class*='p-side-navigation--'] .p-side-navigation__toggle--in-drawer.is-disabled:active,[class*='p-side-navigation--'] .p-side-navigation__toggle--in-drawer.is-disabled:hover{background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0.56)}.p-side-navigation .p-side-navigation__toggle--in-drawer .p-link--external,[class*='p-side-navigation--'] .p-side-navigation__toggle--in-drawer .p-link--external{color:currentColor}.p-side-navigation .p-side-navigation__toggle--in-drawer::before,[class*='p-side-navigation--'] .p-side-navigation__toggle--in-drawer::before{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23666' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E")}.p-side-navigation .p-side-navigation__drawer,[class*='p-side-navigation--'] .p-side-navigation__drawer{background:#fff}.p-side-navigation .p-side-navigation__overlay,[class*='p-side-navigation--'] .p-side-navigation__overlay{background:rgba(17,17,17,0.85)}.p-side-navigation .p-side-navigation__drawer-header,[class*='p-side-navigation--'] .p-side-navigation__drawer-header{background:#fff;border-color:rgba(0,0,0,0.1)}.p-side-navigation .p-side-navigation__list::after,.p-side-navigation.is-dark .p-side-navigation__list::after,.p-side-navigation [class*='p-side-navigation--'].is-dark .p-side-navigation__list::after,[class*='p-side-navigation--'].is-dark .p-side-navigation .p-side-navigation__list::after,[class*='p-side-navigation--'] .p-side-navigation__list::after,[class*='p-side-navigation--'] .p-side-navigation.is-dark .p-side-navigation__list::after,.p-side-navigation.is-dark [class*='p-side-navigation--'] .p-side-navigation__list::after,[class*='p-side-navigation--'].is-dark .p-side-navigation__list::after{background:rgba(0,0,0,0.1)}.p-side-navigation .p-side-navigation__link,.p-side-navigation.is-dark .p-side-navigation__link,.p-side-navigation [class*='p-side-navigation--'].is-dark .p-side-navigation__link,[class*='p-side-navigation--'].is-dark .p-side-navigation .p-side-navigation__link,.p-side-navigation .p-side-navigation__link:visited,[class*='p-side-navigation--'] .p-side-navigation__link,[class*='p-side-navigation--'] .p-side-navigation.is-dark .p-side-navigation__link,.p-side-navigation.is-dark [class*='p-side-navigation--'] .p-side-navigation__link,[class*='p-side-navigation--'].is-dark .p-side-navigation__link,[class*='p-side-navigation--'] .p-side-navigation__link:visited{color:#666}.p-side-navigation .p-side-navigation__link:hover,[class*='p-side-navigation--'] .p-side-navigation__link:hover{background:rgba(0,0,0,0.05);color:#111}.p-side-navigation .p-side-navigation__link:active,[class*='p-side-navigation--'] .p-side-navigation__link:active{background:rgba(0,0,0,0.15)}.p-side-navigation .is-active.p-side-navigation__link,.p-side-navigation .p-side-navigation__link[aria-current='page'],.p-side-navigation .p-side-navigation__link[aria-current='true'],[class*='p-side-navigation--'] .is-active.p-side-navigation__link,[class*='p-side-navigation--'] .p-side-navigation__link[aria-current='page'],[class*='p-side-navigation--'] .p-side-navigation__link[aria-current='true']{position:relative;background:rgba(0,0,0,0.05);color:#111}.p-side-navigation .is-active.p-side-navigation__link::before,.p-side-navigation .p-side-navigation__link[aria-current='page']::before,.p-side-navigation .p-side-navigation__link[aria-current='true']::before,[class*='p-side-navigation--'] .is-active.p-side-navigation__link::before,[class*='p-side-navigation--'] .p-side-navigation__link[aria-current='page']::before,[class*='p-side-navigation--'] .p-side-navigation__link[aria-current='true']::before{left:0;background-color:#111;content:'';position:absolute}.p-side-navigation .is-active.p-side-navigation__link::before,.p-side-navigation .p-side-navigation__link[aria-current='page']::before,.p-side-navigation .p-side-navigation__link[aria-current='true']::before,[class*='p-side-navigation--'] .is-active.p-side-navigation__link::before,[class*='p-side-navigation--'] .p-side-navigation__link[aria-current='page']::before,[class*='p-side-navigation--'] .p-side-navigation__link[aria-current='true']::before{height:auto;width:.1875rem;bottom:0;top:0}.p-side-navigation .p-side-navigation__item--title,.p-side-navigation .p-side-navigation__item--title .p-side-navigation__link,[class*='p-side-navigation--'] .p-side-navigation__item--title,[class*='p-side-navigation--'] .p-side-navigation__item--title .p-side-navigation__link{color:#111}@media (min-width: 772px){.l-navigation .p-side-navigation .p-side-navigation__item.has-active-child,.l-navigation [class*='p-side-navigation--'] .p-side-navigation__item.has-active-child{position:relative;background:rgba(0,0,0,0.05);color:#111}.l-navigation .p-side-navigation .p-side-navigation__item.has-active-child::before,.l-navigation [class*='p-side-navigation--'] .p-side-navigation__item.has-active-child::before{left:0;background-color:#111;content:'';position:absolute}.l-navigation .p-side-navigation .p-side-navigation__item.has-active-child::before,.l-navigation [class*='p-side-navigation--'] .p-side-navigation__item.has-active-child::before{height:auto;width:.1875rem;bottom:0;top:0}}@media (min-width: 772px){.l-navigation.is-pinned .p-side-navigation .p-side-navigation__item.has-active-child,.l-navigation:focus-within .p-side-navigation .p-side-navigation__item.has-active-child,.l-navigation:hover .p-side-navigation .p-side-navigation__item.has-active-child,.l-navigation.is-pinned [class*='p-side-navigation--'] .p-side-navigation__item.has-active-child,.l-navigation:focus-within [class*='p-side-navigation--'] .p-side-navigation__item.has-active-child,.l-navigation:hover [class*='p-side-navigation--'] .p-side-navigation__item.has-active-child{position:relative;background:transparent;color:#666}.l-navigation.is-pinned .p-side-navigation .p-side-navigation__item.has-active-child::before,.l-navigation:focus-within .p-side-navigation .p-side-navigation__item.has-active-child::before,.l-navigation:hover .p-side-navigation .p-side-navigation__item.has-active-child::before,.l-navigation.is-pinned [class*='p-side-navigation--'] .p-side-navigation__item.has-active-child::before,.l-navigation:focus-within [class*='p-side-navigation--'] .p-side-navigation__item.has-active-child::before,.l-navigation:hover [class*='p-side-navigation--'] .p-side-navigation__item.has-active-child::before{left:0;background-color:rgba(0,0,0,0);content:'';position:absolute}.l-navigation.is-pinned .p-side-navigation .p-side-navigation__item.has-active-child::before,.l-navigation:focus-within .p-side-navigation .p-side-navigation__item.has-active-child::before,.l-navigation:hover .p-side-navigation .p-side-navigation__item.has-active-child::before,.l-navigation.is-pinned [class*='p-side-navigation--'] .p-side-navigation__item.has-active-child::before,.l-navigation:focus-within [class*='p-side-navigation--'] .p-side-navigation__item.has-active-child::before,.l-navigation:hover [class*='p-side-navigation--'] .p-side-navigation__item.has-active-child::before{height:auto;width:.1875rem;bottom:0;top:0}}@media (min-width: 1036px){.l-navigation .p-side-navigation .p-side-navigation__item.has-active-child,.l-navigation [class*='p-side-navigation--'] .p-side-navigation__item.has-active-child{position:relative;background:transparent;color:#666}.l-navigation .p-side-navigation .p-side-navigation__item.has-active-child::before,.l-navigation [class*='p-side-navigation--'] .p-side-navigation__item.has-active-child::before{left:0;background-color:rgba(0,0,0,0);content:'';position:absolute}.l-navigation .p-side-navigation .p-side-navigation__item.has-active-child::before,.l-navigation [class*='p-side-navigation--'] .p-side-navigation__item.has-active-child::before{height:auto;width:.1875rem;bottom:0;top:0}}.p-side-navigation.is-dark,[class*='p-side-navigation--'].is-dark{color:rgba(255,255,255,0.55)}.p-side-navigation.is-dark .p-side-navigation__toggle,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle{background-color:#fff;border-color:rgba(0,0,0,0.56);color:#111}.p-side-navigation.is-dark .p-side-navigation__toggle:visited,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle:visited{color:#111}.p-side-navigation.is-dark .p-side-navigation__toggle:hover,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle:hover{background-color:#f2f2f2;border-color:rgba(0,0,0,0.56)}.p-side-navigation.is-dark .p-side-navigation__toggle:active,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle:active{background-color:#d9d9d9;border-color:rgba(0,0,0,0.56);transition-duration:0s}.p-side-navigation.is-dark .p-side-navigation__toggle:disabled:active,.p-side-navigation.is-dark .p-side-navigation__toggle:disabled:hover,.p-side-navigation.is-dark .p-side-navigation__toggle.is-disabled:active,.p-side-navigation.is-dark .p-side-navigation__toggle.is-disabled:hover,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle:disabled:active,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle:disabled:hover,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle.is-disabled:active,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle.is-disabled:hover{background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0.56)}.p-side-navigation.is-dark .p-side-navigation__toggle .p-link--external,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle .p-link--external{color:currentColor}.p-side-navigation.is-dark .p-side-navigation__toggle::before,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle::before{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23666' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E")}.p-side-navigation.is-dark .p-side-navigation__toggle--in-drawer,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle--in-drawer{background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0);color:rgba(255,255,255,0.55)}.p-side-navigation.is-dark .p-side-navigation__toggle--in-drawer:visited,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle--in-drawer:visited{color:rgba(255,255,255,0.55)}.p-side-navigation.is-dark .p-side-navigation__toggle--in-drawer:hover,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle--in-drawer:hover{background-color:rgba(255,255,255,0.05);border-color:rgba(0,0,0,0)}.p-side-navigation.is-dark .p-side-navigation__toggle--in-drawer:active,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle--in-drawer:active{background-color:rgba(255,255,255,0.15);border-color:rgba(0,0,0,0);transition-duration:0s}.p-side-navigation.is-dark .p-side-navigation__toggle--in-drawer:disabled:active,.p-side-navigation.is-dark .p-side-navigation__toggle--in-drawer:disabled:hover,.p-side-navigation.is-dark .p-side-navigation__toggle--in-drawer.is-disabled:active,.p-side-navigation.is-dark .p-side-navigation__toggle--in-drawer.is-disabled:hover,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle--in-drawer:disabled:active,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle--in-drawer:disabled:hover,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle--in-drawer.is-disabled:active,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle--in-drawer.is-disabled:hover{background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0.56)}.p-side-navigation.is-dark .p-side-navigation__toggle--in-drawer .p-link--external,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle--in-drawer .p-link--external{color:currentColor}.p-side-navigation.is-dark .p-side-navigation__toggle--in-drawer::before,[class*='p-side-navigation--'].is-dark .p-side-navigation__toggle--in-drawer::before{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23999' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E")}.p-side-navigation.is-dark .p-side-navigation__drawer,[class*='p-side-navigation--'].is-dark .p-side-navigation__drawer{background:#262626}.p-side-navigation.is-dark .p-side-navigation__overlay,[class*='p-side-navigation--'].is-dark .p-side-navigation__overlay{background:rgba(17,17,17,0.85)}.p-side-navigation.is-dark .p-side-navigation__drawer-header,[class*='p-side-navigation--'].is-dark .p-side-navigation__drawer-header{background:#262626;border-color:rgba(255,255,255,0.1)}.p-side-navigation.is-dark .p-side-navigation__list::after,[class*='p-side-navigation--'].is-dark .p-side-navigation__list::after{background:rgba(255,255,255,0.1)}.p-side-navigation.is-dark .p-side-navigation__link,.p-side-navigation.is-dark .p-side-navigation__link:visited,[class*='p-side-navigation--'].is-dark .p-side-navigation__link,[class*='p-side-navigation--'].is-dark .p-side-navigation__link:visited{color:rgba(255,255,255,0.55)}.p-side-navigation.is-dark .p-side-navigation__link:hover,[class*='p-side-navigation--'].is-dark .p-side-navigation__link:hover{background:rgba(255,255,255,0.05);color:#fff}.p-side-navigation.is-dark .p-side-navigation__link:active,[class*='p-side-navigation--'].is-dark .p-side-navigation__link:active{background:rgba(255,255,255,0.15)}.p-side-navigation.is-dark .is-active.p-side-navigation__link,.p-side-navigation.is-dark .p-side-navigation__link[aria-current='page'],.p-side-navigation.is-dark .p-side-navigation__link[aria-current='true'],[class*='p-side-navigation--'].is-dark .is-active.p-side-navigation__link,[class*='p-side-navigation--'].is-dark .p-side-navigation__link[aria-current='page'],[class*='p-side-navigation--'].is-dark .p-side-navigation__link[aria-current='true']{position:relative;background:rgba(255,255,255,0.05);color:#fff}.p-side-navigation.is-dark .is-active.p-side-navigation__link::before,.p-side-navigation.is-dark .p-side-navigation__link[aria-current='page']::before,.p-side-navigation.is-dark .p-side-navigation__link[aria-current='true']::before,[class*='p-side-navigation--'].is-dark .is-active.p-side-navigation__link::before,[class*='p-side-navigation--'].is-dark .p-side-navigation__link[aria-current='page']::before,[class*='p-side-navigation--'].is-dark .p-side-navigation__link[aria-current='true']::before{left:0;background-color:#fff;content:'';position:absolute}.p-side-navigation.is-dark .is-active.p-side-navigation__link::before,.p-side-navigation.is-dark .p-side-navigation__link[aria-current='page']::before,.p-side-navigation.is-dark .p-side-navigation__link[aria-current='true']::before,[class*='p-side-navigation--'].is-dark .is-active.p-side-navigation__link::before,[class*='p-side-navigation--'].is-dark .p-side-navigation__link[aria-current='page']::before,[class*='p-side-navigation--'].is-dark .p-side-navigation__link[aria-current='true']::before{height:auto;width:.1875rem;bottom:0;top:0}.p-side-navigation.is-dark .p-side-navigation__item--title,.p-side-navigation.is-dark .p-side-navigation__item--title .p-side-navigation__link,[class*='p-side-navigation--'].is-dark .p-side-navigation__item--title,[class*='p-side-navigation--'].is-dark .p-side-navigation__item--title .p-side-navigation__link{color:#fff}@media (min-width: 772px){.l-navigation .p-side-navigation.is-dark .p-side-navigation__item.has-active-child,.l-navigation [class*='p-side-navigation--'].is-dark .p-side-navigation__item.has-active-child{position:relative;background:rgba(255,255,255,0.05);color:#fff}.l-navigation .p-side-navigation.is-dark .p-side-navigation__item.has-active-child::before,.l-navigation [class*='p-side-navigation--'].is-dark .p-side-navigation__item.has-active-child::before{left:0;background-color:#fff;content:'';position:absolute}.l-navigation .p-side-navigation.is-dark .p-side-navigation__item.has-active-child::before,.l-navigation [class*='p-side-navigation--'].is-dark .p-side-navigation__item.has-active-child::before{height:auto;width:.1875rem;bottom:0;top:0}}@media (min-width: 772px){.l-navigation.is-pinned .p-side-navigation.is-dark .p-side-navigation__item.has-active-child,.l-navigation:focus-within .p-side-navigation.is-dark .p-side-navigation__item.has-active-child,.l-navigation:hover .p-side-navigation.is-dark .p-side-navigation__item.has-active-child,.l-navigation.is-pinned [class*='p-side-navigation--'].is-dark .p-side-navigation__item.has-active-child,.l-navigation:focus-within [class*='p-side-navigation--'].is-dark .p-side-navigation__item.has-active-child,.l-navigation:hover [class*='p-side-navigation--'].is-dark .p-side-navigation__item.has-active-child{position:relative;background:transparent;color:rgba(255,255,255,0.55)}.l-navigation.is-pinned .p-side-navigation.is-dark .p-side-navigation__item.has-active-child::before,.l-navigation:focus-within .p-side-navigation.is-dark .p-side-navigation__item.has-active-child::before,.l-navigation:hover .p-side-navigation.is-dark .p-side-navigation__item.has-active-child::before,.l-navigation.is-pinned [class*='p-side-navigation--'].is-dark .p-side-navigation__item.has-active-child::before,.l-navigation:focus-within [class*='p-side-navigation--'].is-dark .p-side-navigation__item.has-active-child::before,.l-navigation:hover [class*='p-side-navigation--'].is-dark .p-side-navigation__item.has-active-child::before{left:0;background-color:rgba(0,0,0,0);content:'';position:absolute}.l-navigation.is-pinned .p-side-navigation.is-dark .p-side-navigation__item.has-active-child::before,.l-navigation:focus-within .p-side-navigation.is-dark .p-side-navigation__item.has-active-child::before,.l-navigation:hover .p-side-navigation.is-dark .p-side-navigation__item.has-active-child::before,.l-navigation.is-pinned [class*='p-side-navigation--'].is-dark .p-side-navigation__item.has-active-child::before,.l-navigation:focus-within [class*='p-side-navigation--'].is-dark .p-side-navigation__item.has-active-child::before,.l-navigation:hover [class*='p-side-navigation--'].is-dark .p-side-navigation__item.has-active-child::before{height:auto;width:.1875rem;bottom:0;top:0}}@media (min-width: 1036px){.l-navigation .p-side-navigation.is-dark .p-side-navigation__item.has-active-child,.l-navigation [class*='p-side-navigation--'].is-dark .p-side-navigation__item.has-active-child{position:relative;background:transparent;color:rgba(255,255,255,0.55)}.l-navigation .p-side-navigation.is-dark .p-side-navigation__item.has-active-child::before,.l-navigation [class*='p-side-navigation--'].is-dark .p-side-navigation__item.has-active-child::before{left:0;background-color:rgba(0,0,0,0);content:'';position:absolute}.l-navigation .p-side-navigation.is-dark .p-side-navigation__item.has-active-child::before,.l-navigation [class*='p-side-navigation--'].is-dark .p-side-navigation__item.has-active-child::before{height:auto;width:.1875rem;bottom:0;top:0}}.p-side-navigation--raw-html ul::after{background:rgba(0,0,0,0.1)}.p-side-navigation--raw-html li>a,.p-side-navigation--raw-html li>a:visited{color:#666}.p-side-navigation--raw-html li>a:hover{background:rgba(0,0,0,0.05);color:#111}.p-side-navigation--raw-html li>a:active{background:rgba(0,0,0,0.15)}.p-side-navigation--raw-html li>a.is-active,.p-side-navigation--raw-html li>a[aria-current='page'],.p-side-navigation--raw-html li>a[aria-current='true']{position:relative;background:rgba(0,0,0,0.05);color:#111}.p-side-navigation--raw-html li>a.is-active::before,.p-side-navigation--raw-html li>a[aria-current='page']::before,.p-side-navigation--raw-html li>a[aria-current='true']::before{left:0;background-color:#111;content:'';position:absolute}.p-side-navigation--raw-html li>a.is-active::before,.p-side-navigation--raw-html li>a[aria-current='page']::before,.p-side-navigation--raw-html li>a[aria-current='true']::before{height:auto;width:.1875rem;bottom:0;top:0}.p-side-navigation--raw-html h2,.p-side-navigation--raw-html h3,.p-side-navigation--raw-html h4,.p-side-navigation--raw-html h5,.p-side-navigation--raw-html h6{color:#111}.p-side-navigation--raw-html.is-light ul::after{background:rgba(255,255,255,0.1)}.p-side-navigation--raw-html.is-light li>a,.p-side-navigation--raw-html.is-light li>a:visited{color:rgba(255,255,255,0.55)}.p-side-navigation--raw-html.is-light li>a:hover{background:rgba(255,255,255,0.05);color:#fff}.p-side-navigation--raw-html.is-light li>a:active{background:rgba(255,255,255,0.15)}.p-side-navigation--raw-html.is-light li>a.is-active,.p-side-navigation--raw-html.is-light li>a[aria-current='page'],.p-side-navigation--raw-html.is-light li>a[aria-current='true']{position:relative;background:rgba(255,255,255,0.05);color:#fff}.p-side-navigation--raw-html.is-light li>a.is-active::before,.p-side-navigation--raw-html.is-light li>a[aria-current='page']::before,.p-side-navigation--raw-html.is-light li>a[aria-current='true']::before{left:0;background-color:#fff;content:'';position:absolute}.p-side-navigation--raw-html.is-light li>a.is-active::before,.p-side-navigation--raw-html.is-light li>a[aria-current='page']::before,.p-side-navigation--raw-html.is-light li>a[aria-current='true']::before{height:auto;width:.1875rem;bottom:0;top:0}.p-side-navigation--raw-html.is-light h2,.p-side-navigation--raw-html.is-light h3,.p-side-navigation--raw-html.is-light h4,.p-side-navigation--raw-html.is-light h5,.p-side-navigation--raw-html.is-light h6{color:#fff}.p-slider__wrapper{align-items:center;display:inline-flex;width:100%}.p-slider__input{height:2.625rem;margin:0 0 0 1rem;min-width:5rem;text-align:center;width:5%}.p-strip,.p-strip--light,.p-strip--dark,.p-strip--accent,.p-strip--image,.p-strip--suru,.p-strip--suru-topped{clear:both;position:relative;width:100%}.p-strip{background-color:transparent}.p-strip--light{background-color:#f7f7f7}.p-strip--dark{background-color:#111;color:#f7f7f7}.p-strip--accent{background-color:#333;color:#fff}.p-strip--image{background-repeat:no-repeat;background-size:cover}.p-strip--image.is-light{color:#111}.p-strip--image.is-dark{color:#fff}.p-strip--suru{background-image:linear-gradient(to bottom right, rgba(205,205,205,0.14) 0%, rgba(205,205,205,0.14) 49.8%, transparent 50%, transparent 100%),linear-gradient(to bottom left, rgba(205,205,205,0.14) 0%, rgba(205,205,205,0.14) 49.8%, transparent 50%, transparent 100%),linear-gradient(to top right, #fff 0%, #fff 49%, transparent 50%, transparent 100%),linear-gradient(to top right, #fff 0%, #fff 100%),linear-gradient(111deg, #4d4d4d 10%, #333 37%, #1a1a1a 100%);background-position:0% 0%, top right, right 0 bottom 4rem, right bottom, 0% 0%;background-repeat:no-repeat;background-size:100% calc(100% - 4rem),50% 100%,100% 4rem,100% 4rem,auto;color:#fff;margin-bottom:-4rem;overflow:hidden;padding-bottom:12rem;position:relative}@supports (background-blend-mode: multiply){.p-strip--suru{background-blend-mode:multiply, multiply, normal, normal, normal;background-image:linear-gradient(to bottom right, rgba(205,205,205,0.55) 0%, rgba(205,205,205,0.55) 49.8%, transparent 50%, transparent 100%),linear-gradient(to bottom left, rgba(205,205,205,0.55) 0%, rgba(205,205,205,0.55) 49.8%, transparent 50%, transparent 100%),linear-gradient(to top right, #fff 0%, #fff 49%, transparent 50%, transparent 100%),linear-gradient(#fff 0%, #fff 100%),linear-gradient(111deg, #4d4d4d 10%, #333 37%, #1a1a1a 100%)}}.p-strip--suru.is-deep{background-position:0% 0%,top right,right 0 bottom 3rem,right bottom,0% 0%;background-size:100% calc(100% - 3rem),100% 100%,100% 3rem,100% 3rem,auto;margin-bottom:-3rem;padding-bottom:9rem !important}@media (min-width: 772px){.p-strip--suru.is-deep{background-position:0% 0%,top right,right 0 bottom 6rem,right bottom,0% 0%;background-size:100% calc(100% - 6rem),50% 100%,100% 6rem,100% 6rem,auto;margin-bottom:-6rem;padding-bottom:18rem !important}}.p-strip--suru.is-shallow{padding:4rem 0 12rem 0}.p-strip--suru-topped{background-image:linear-gradient(to bottom left, rgba(229,229,229,0.14) 0%, rgba(229,229,229,0.14) 49%, transparent 50%, transparent 100%),linear-gradient(to bottom left, rgba(51,51,51,0.14) 0%, rgba(51,51,51,0.14) 49%, transparent 50%, transparent 100%),linear-gradient(to bottom right, transparent 0%, transparent 49%, #fff 50%, #fff 100%),linear-gradient(90deg, #4d4d4d 4%, #333 50%, #1a1a1a 88%);background-position:top right, top right, top left, top left;background-repeat:no-repeat;background-size:39.4% 6rem, 54% 4rem, 63% 4rem, 62.6% 4rem;padding-bottom:4rem;padding-top:6rem}@supports (background-blend-mode: multiply){.p-strip--suru-topped{background-blend-mode:multiply, multiply, normal, normal;background-image:linear-gradient(to bottom left, rgba(229,229,229,0.5) 0%, rgba(229,229,229,0.5) 49%, transparent 50%, transparent 100%),linear-gradient(to bottom left, rgba(51,51,51,0.16) 0%, rgba(51,51,51,0.16) 49%, transparent 50%, transparent 100%),linear-gradient(to bottom right, transparent 0%, transparent 49%, #fff 50%, #fff 100%),linear-gradient(90deg, #4d4d4d 4%, #333 50%, #1a1a1a 88%)}}.p-strip--suru-topped.is-shallow{padding:6rem 0 4rem 0}.p-switch{height:1.5rem;margin:0;opacity:0 !important;position:absolute !important;width:3rem}.p-switch:checked+.p-switch__slider::before{left:50%}.p-switch:focus{outline:none}.p-switch:focus+.p-switch__slider{outline:.1875rem solid #2e96ff}.p-switch__slider{background:linear-gradient(to right, #06c 50%, #cdcdcd 50%);box-shadow:inset 0 2px 5px 0 rgba(17,17,17,0.2);display:block;height:1.5rem;margin:.1rem 0 1rem 0;position:relative;width:3rem}.p-switch__slider::before{transition-duration:0.5s;transition-property:all;transition-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1);background:#fff;content:'';height:1.5rem;left:0;position:absolute;width:1.5rem}.p-table__cell--icon-placeholder{padding-left:2rem}.p-table__cell--icon-placeholder [class^='p-icon']:first-child{margin-left:-1.5rem;margin-right:.5rem}.p-table-expanding{display:flex;flex-flow:column nowrap;justify-content:space-between}.p-table-expanding thead,.p-table-expanding tbody{display:block;margin:0}.p-table-expanding tr{display:flex;margin:0;width:100%;flex-flow:row;flex-wrap:wrap}.p-table-expanding tr+tr{margin:0}.p-table-expanding th,.p-table-expanding td{align-items:flex-start;display:flex;margin:0;word-break:break-word;flex-basis:0;flex-flow:row nowrap;flex-grow:1}.p-table-expanding th.p-table-expanding__panel,.p-table-expanding td.p-table-expanding__panel{flex-basis:100%;max-width:100%}.p-table-expanding th.p-table-expanding__panel[aria-hidden='true'],.p-table-expanding td.p-table-expanding__panel[aria-hidden='true']{display:none}.p-table-expanding th.p-table-expanding__panel .row,.p-table-expanding td.p-table-expanding__panel .row{max-width:100%;padding:0;width:100%}.p-table-expanding th[aria-hidden='true'],.p-table-expanding td[aria-hidden='true']{display:none}.p-table-of-contents{border-top:1px solid #cdcdcd;font-size:0.875rem;padding:0 1.5rem}@media (min-width: 772px){.p-table-of-contents{border-left:1px solid #cdcdcd;border-top:0;padding:0 1rem}}.p-table-of-contents__header{color:#666;font-size:1rem;line-height:1.5;margin-bottom:1rem;text-transform:uppercase}.p-table-of-contents__section{padding:1rem 0}.p-table-of-contents__section:not(:last-child){border-bottom:1px dotted #cdcdcd}.p-table-of-contents__nav{list-style:none;margin:0;padding:0}.p-table-of-contents__nav .p-table-of-contents__link{border-bottom:0;color:#111;margin-bottom:.25rem}.p-table-of-contents__nav .p-table-of-contents__link:visited{color:#111}.p-table-of-contents__nav .p-table-of-contents__link:hover{color:#06c}.p-table-of-contents__nav .p-table-of-contents__link.is-active{font-weight:400;padding-left:.25rem}@media screen and (max-width: 772px){.p-table--mobile-card thead{display:none}.p-table--mobile-card tbody{display:flex;flex-wrap:wrap;justify-content:space-between}.p-table--mobile-card tr{border:1px solid #cdcdcd;border-radius:.125rem;display:flex;flex-direction:column;margin-bottom:1.5rem;overflow:auto;padding:0 1.5rem calc(1rem - 1px);width:calc(50% - 1rem)}.p-table--mobile-card td,.p-table--mobile-card tbody th{display:flex;min-width:100%;overflow:hidden;padding-bottom:.1rem;padding-left:calc(50% + 0.5rem);padding-right:0;padding-top:.4rem;position:relative;text-align:left !important;text-overflow:ellipsis;width:100%;word-break:break-word}.p-table--mobile-card td[aria-label],.p-table--mobile-card tbody th[aria-label]{text-align:right}.p-table--mobile-card td[aria-label]::before,.p-table--mobile-card tbody th[aria-label]::before{content:attr(aria-label);display:block;flex:0 0 auto;margin-bottom:.3rem;margin-left:-100%;overflow:hidden;padding-right:1rem;padding-top:.3rem;text-align:right;text-overflow:ellipsis;width:100%}.p-table--mobile-card td.u-align--right,.p-table--mobile-card tbody th.u-align--right{justify-content:unset !important}.p-table--mobile-card .p-contextual-menu{width:100%}.p-table--mobile-card .p-contextual-menu [role='menuitem']{display:none}.p-table--mobile-card .p-contextual-menu__dropdown{box-shadow:none;display:block;max-width:100%;position:relative}.p-table--mobile-card .p-contextual-menu__dropdown::before{display:none}.p-table--mobile-card .p-contextual-menu__group{padding:0}.p-table--mobile-card .p-contextual-menu__group+.p-contextual-menu__group{margin-top:.5rem;padding-top:.5rem}.p-table--mobile-card .p-contextual-menu__link{border-color:#cdcdcd;border-radius:0.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#000;cursor:pointer;display:block;line-height:1rem;outline:none;padding:.5rem 1.5rem;text-align:center;text-decoration:none;width:100%}.p-table--mobile-card .p-contextual-menu__link+.p-contextual-menu__link{margin-top:.25rem}}@media screen and (max-width: 620px){.p-table--mobile-card tr{width:100%}}.p-table--sortable thead th[aria-sort='ascending']::after,.p-table--sortable thead th[aria-sort='descending']::after{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23666' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E");background-size:contain;height:1rem;width:1rem;background-repeat:no-repeat;background-size:100%;content:'';display:inline-block;margin-left:.25rem;margin-top:calc(.76562rem - 1rem);vertical-align:calc(.5px + 0.3465em - 0.5rem)}.p-table--sortable{table-layout:fixed}.p-table--sortable thead th[aria-sort]{align-items:center;cursor:pointer;white-space:nowrap}.p-table--sortable thead th[aria-sort='descending']::after{transform:rotate(180deg)}.p-table--sortable thead th[aria-sort]:hover{color:#06c;text-decoration:underline}.p-tabs{border-radius:0;overflow:hidden;padding:0;position:relative}.p-tabs::after{background:linear-gradient(to right, rgba(0,0,0,0) 0%, #fff 45%, #fff 100%);color:#666;content:'\203A';display:block;font-size:2rem;line-height:calc(100% + 1rem - .1875rem);padding-left:1.5rem;padding-right:1.5rem;pointer-events:none;position:absolute;right:0;text-align:right;top:0;width:1rem}@media screen and (min-width: 772px){.p-tabs::after{display:none}}.p-tabs__list{display:flex;margin:0 auto 1.5rem;overflow-x:auto;padding:0;position:relative;white-space:nowrap;width:100%}.p-tabs__item{margin:0;padding:0;width:auto}.p-tabs__item:last-child{margin-right:3rem}.p-tabs__link{color:#111;display:block;line-height:1.5rem;padding:.75rem 1rem;position:relative}.p-tabs__link:focus{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-tabs__link:focus-visible{outline:.1875rem solid #2e96ff;outline-offset:-.1875rem}.p-tabs__link:focus:not(:focus-visible){outline:0;outline-offset:0}.p-tabs__link:active,.p-tabs__link:hover,.p-tabs__link:visited{color:#111;text-decoration:none}.p-tabs__link::before{bottom:0;z-index:1}.p-tabs__link:focus{z-index:1}.p-tabs__link:focus::before,.p-tabs__link:focus::after{content:none}.p-tabs__link:hover,.p-tabs__link[aria-selected='true']{position:relative}.p-tabs__link:hover::before,.p-tabs__link[aria-selected='true']::before{bottom:0;background-color:#666;content:'';position:absolute}.p-tabs__link:hover::before,.p-tabs__link[aria-selected='true']::before{height:.1875rem;width:auto;left:0;right:0}.p-tabs__link:hover:focus::before,.p-tabs__link:hover:focus::after,.p-tabs__link[aria-selected='true']:focus::before,.p-tabs__link[aria-selected='true']:focus::after{content:none}.p-tooltip,.p-tooltip--btm-center,.p-tooltip--btm-right,.p-tooltip--top-left,.p-tooltip--top-center,.p-tooltip--top-right,.p-tooltip--right,.p-tooltip--left{position:relative}.p-tooltip:focus .p-tooltip__message,.p-tooltip--btm-center:focus .p-tooltip__message,.p-tooltip--btm-right:focus .p-tooltip__message,.p-tooltip--top-left:focus .p-tooltip__message,.p-tooltip--top-center:focus .p-tooltip__message,.p-tooltip--top-right:focus .p-tooltip__message,.p-tooltip--right:focus .p-tooltip__message,.p-tooltip--left:focus .p-tooltip__message,.p-tooltip:hover .p-tooltip__message,.p-tooltip--btm-center:hover .p-tooltip__message,.p-tooltip--btm-right:hover .p-tooltip__message,.p-tooltip--top-left:hover .p-tooltip__message,.p-tooltip--top-center:hover .p-tooltip__message,.p-tooltip--top-right:hover .p-tooltip__message,.p-tooltip--right:hover .p-tooltip__message,.p-tooltip--left:hover .p-tooltip__message{display:inline;text-decoration:initial}.p-tooltip__message{background-color:#111;border:0;border-radius:.125rem;color:#fff;display:none;left:-1rem;margin-bottom:0;padding:.5rem 1rem;position:absolute;text-align:left;text-decoration:initial;top:100%;transform:translateX(0%) translateY(13px);white-space:pre;z-index:1}.p-tooltip__message::before{border-bottom:.5rem solid #111;border-left:.5rem solid transparent;border-right:.5rem solid transparent;bottom:100%;content:'';height:0;left:1rem;pointer-events:none;position:absolute;width:0}.p-tooltip__message::after{border-radius:10% 90% 0% 100%/100% 100% 0% 0%;content:'';height:1rem;left:0;position:absolute;right:0;top:-1rem}.p-tooltip--btm-center .p-tooltip__message{bottom:inherit;left:50%;top:100%;transform:translateX(-50%) translateY(13px)}.p-tooltip--btm-center .p-tooltip__message::before{left:50%;transform:translateX(-50%)}.p-tooltip--btm-center .p-tooltip__message::after{border-radius:50% 50% 0% 100%/100% 100% 0% 0%;height:1rem}.p-tooltip--btm-right .p-tooltip__message{bottom:inherit;left:initial;right:-1rem;top:100%;transform:translateY(13px)}.p-tooltip--btm-right .p-tooltip__message::before{left:initial;right:1rem}.p-tooltip--btm-right .p-tooltip__message::after{border-radius:90% 10% 0% 100%/100% 100% 0% 0%;height:1rem}.p-tooltip--top-left .p-tooltip__message{bottom:100%;left:-1rem;top:initial;transform:translateX(0%) translateY(-13px)}.p-tooltip--top-left .p-tooltip__message::before{border-bottom:.5rem solid transparent;border-left:.5rem solid transparent;border-right:.5rem solid transparent;border-top:.5rem solid #111;bottom:-1rem;left:1rem}.p-tooltip--top-left .p-tooltip__message::after{border-radius:0% 100% 90% 10%/0% 0% 100% 100%;bottom:-1rem;height:1rem;top:auto}.p-tooltip--top-center .p-tooltip__message{bottom:100%;left:50%;top:initial;transform:translateX(-50%) translateY(-13px)}.p-tooltip--top-center .p-tooltip__message::before{border-bottom:.5rem solid transparent;border-left:.5rem solid transparent;border-right:.5rem solid transparent;border-top:.5rem solid #111;bottom:-1rem;left:50%;transform:translateX(-50%)}.p-tooltip--top-center .p-tooltip__message::after{border-radius:100% 0% 50% 50%/0% 0% 100% 100%;bottom:-1rem;height:1rem;top:auto}.p-tooltip--top-right .p-tooltip__message{bottom:100%;left:initial;right:-1rem;top:initial;transform:translateX(0%) translateY(-13px)}.p-tooltip--top-right .p-tooltip__message::before{border-bottom:.5rem solid transparent;border-left:.5rem solid transparent;border-right:.5rem solid transparent;border-top:.5rem solid #111;bottom:-1rem;left:initial;right:1rem}.p-tooltip--top-right .p-tooltip__message::after{border-radius:100% 0% 10% 90%/0% 0% 100% 100%;bottom:-1rem;height:1rem;top:auto}.p-tooltip--right .p-tooltip__message{bottom:inherit;left:100%;top:50%;transform:translateX(14px) translateY(-50%)}.p-tooltip--right .p-tooltip__message::before{border-bottom:.5rem solid transparent;border-left:.5rem solid transparent;border-right:.5rem solid #111;border-top:.5rem solid transparent;bottom:inherit;left:0;top:50%;transform:translateX(-16px) translateY(-50%)}.p-tooltip--right .p-tooltip__message::after{border-radius:0;bottom:0;height:auto;left:-1rem;right:auto;top:0;width:1rem}.p-tooltip--left .p-tooltip__message{bottom:inherit;left:-16px;top:50%;transform:translateX(-100%) translateY(-50%)}.p-tooltip--left .p-tooltip__message::before{border-bottom:.5rem solid transparent;border-left:.5rem solid #111;border-right:.5rem solid transparent;border-top:.5rem solid transparent;bottom:inherit;left:100%;top:50%;transform:translateX(0) translateY(-50%)}.p-tooltip--left .p-tooltip__message::after{border-radius:0;bottom:0;height:auto;left:auto;right:-1rem;top:0;width:1rem}.l-application{display:grid;grid-template-areas:'nav navbar navbar' 'nav main aside' 'nav status status';grid-template-columns:-webkit-min-content minmax(0, 1fr) minmax(0, -webkit-min-content);grid-template-columns:min-content minmax(0, 1fr) minmax(0, min-content);grid-template-rows:-webkit-min-content 1fr -webkit-min-content;grid-template-rows:min-content 1fr min-content;height:100vh;width:100vw}.l-navigation-bar{grid-area:navbar}.l-navigation{transition-duration:0.165s;transition-property:transform;transition-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1);bottom:0;box-shadow:0 0 2rem 0 rgba(0,0,0,0.2);height:100vh;left:0;overflow-y:auto;position:fixed;top:0;transform:translateX(0);width:100%;z-index:103}@media (min-width: 460px){.l-navigation{width:auto}}.l-navigation.is-collapsed{transform:translateX(-100%)}.l-navigation.is-collapsed:focus-within{transform:none}.l-navigation__drawer{height:100vh;width:auto}@media (min-width: 460px){.l-navigation__drawer{width:15rem}}@media (min-width: 772px){.l-navigation-bar{grid-area:nav;overflow:hidden;visibility:hidden;width:4rem}.l-navigation{transition-duration:0.165s;transition-property:width,box-shadow;transition-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1);box-shadow:0 0 0 0 transparent;overflow:hidden;transform:translateX(0);width:4rem}.l-navigation.is-collapsed{transform:translateX(0);width:4rem}.l-navigation:hover,.l-navigation:focus-within,.l-navigation.is-pinned{overflow-y:auto;width:15rem}.l-navigation:hover{box-shadow:0 0 2rem 0 rgba(0,0,0,0.2)}.l-navigation.is-pinned{box-shadow:0 0 0 0 transparent;grid-area:nav;position:static}}@media (min-width: 1036px){.l-navigation-bar{display:none}.l-navigation{box-shadow:none;grid-area:nav;overflow-y:auto;position:static;width:15rem}.l-navigation:hover{box-shadow:none}.l-navigation.is-collapsed{transform:translateX(0);width:15rem}}@media (min-width: 772px){.l-navigation .is-fading-when-collapsed,.l-navigation .p-side-navigation .p-side-navigation__label,.l-navigation [class*='p-side-navigation--'] .p-side-navigation__label,.l-navigation .p-side-navigation .p-side-navigation__list::after,.l-navigation [class*='p-side-navigation--'] .p-side-navigation__list::after{transition-duration:0.1s;transition-property:opacity;transition-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1);opacity:0}}@media (min-width: 772px){.l-navigation.is-pinned .is-fading-when-collapsed,.l-navigation.is-pinned .p-side-navigation .p-side-navigation__label,.l-navigation.is-pinned [class*='p-side-navigation--'] .p-side-navigation__label,.l-navigation.is-pinned .p-side-navigation .p-side-navigation__list::after,.l-navigation.is-pinned [class*='p-side-navigation--'] .p-side-navigation__list::after,.l-navigation:focus-within .is-fading-when-collapsed,.l-navigation:focus-within .p-side-navigation .p-side-navigation__label,.l-navigation:focus-within [class*='p-side-navigation--'] .p-side-navigation__label,.l-navigation:focus-within .p-side-navigation .p-side-navigation__list::after,.l-navigation:focus-within [class*='p-side-navigation--'] .p-side-navigation__list::after,.l-navigation:hover .is-fading-when-collapsed,.l-navigation:hover .p-side-navigation .p-side-navigation__label,.l-navigation:hover [class*='p-side-navigation--'] .p-side-navigation__label,.l-navigation:hover .p-side-navigation .p-side-navigation__list::after,.l-navigation:hover [class*='p-side-navigation--'] .p-side-navigation__list::after{opacity:1}}@media (min-width: 1036px){.l-navigation .is-fading-when-collapsed,.l-navigation .p-side-navigation .p-side-navigation__label,.l-navigation [class*='p-side-navigation--'] .p-side-navigation__label,.l-navigation .p-side-navigation .p-side-navigation__list::after,.l-navigation [class*='p-side-navigation--'] .p-side-navigation__list::after{opacity:1}}@media (min-width: 772px){.l-navigation .p-side-navigation__list .p-side-navigation__list{display:none}}@media (min-width: 772px){.l-navigation.is-pinned .p-side-navigation__list .p-side-navigation__list,.l-navigation:focus-within .p-side-navigation__list .p-side-navigation__list,.l-navigation:hover .p-side-navigation__list .p-side-navigation__list{display:block}}@media (min-width: 1036px){.l-navigation .p-side-navigation__list .p-side-navigation__list{display:block}}@media (min-width: 772px){.l-navigation .p-side-navigation__item{white-space:nowrap}}@media (min-width: 772px){.l-navigation.is-pinned .p-side-navigation__item,.l-navigation:focus-within .p-side-navigation__item,.l-navigation:hover .p-side-navigation__item{white-space:normal}}@media (min-width: 1036px){.l-navigation .p-side-navigation__item{white-space:normal}}.l-main{grid-area:main;overflow-y:auto}.l-status{border-top:1px solid rgba(0,0,0,0.1);grid-area:status;z-index:102}.l-aside{box-shadow:0 0 2rem 0 rgba(0,0,0,0.2);grid-area:main;justify-self:right;overflow-y:auto;width:100%;z-index:101}@media (min-width: 460px){.l-aside{max-width:100%;width:45.3rem}.l-aside.is-wide{width:72rem}.l-aside.is-narrow{width:21.65rem}}@media (min-width: 772px){.l-aside.is-pinned{border-left:1px solid rgba(0,0,0,0.1);box-shadow:none;grid-area:aside;justify-self:auto;max-width:50vw}}.p-panel{background:#fff;color:#111;max-width:100%;min-height:100%}.p-panel.is-dark{background:#262626;color:#fff}.p-panel__header{display:flex}.p-panel__header.is-sticky{position:-webkit-sticky;position:sticky;top:0;z-index:1}.p-panel.is-dark .p-panel__header.is-sticky{background:#262626}.p-panel__content{overflow:hidden}.p-panel__logo{align-items:center;display:flex;margin-bottom:1.25rem;margin-left:-0.25rem;margin-top:1.25rem}.p-panel__logo .p-panel__logo-icon{height:1.5rem}.p-panel__logo .p-panel__logo-name{height:1rem;margin-left:0.5rem}.p-panel__title{margin:0;padding-bottom:1rem;padding-top:1rem}.p-panel__controls{margin-left:auto;padding-top:1rem}.p-panel__toggle{cursor:pointer;display:block;margin-bottom:1.25rem;margin-top:0.25rem}.l-application.app-demo>.l-navigation-bar,.l-application.app-demo>.l-navigation .l-navigation__drawer,.l-application.app-demo>.l-main,.l-application.app-demo>.l-aside,.l-application.app-demo>.l-status{padding:1rem}.l-application.app-demo>.l-navigation-bar,.l-application.app-demo>.l-navigation .l-navigation__drawer{background:#111;color:#fff}.l-application.app-demo>.l-navigation-bar{padding-bottom:.25rem;padding-top:.25rem}.l-application.app-demo>.l-aside{background:#fff}.l-application.app-demo>.l-status{background:#f7f7f7;padding-bottom:.5rem;padding-top:.5rem}.u-align--center{justify-content:center !important;text-align:center !important}.u-align--left{justify-content:flex-start !important;text-align:left !important}.u-align--right{justify-content:flex-end !important;text-align:right !important}.u-align--bottom{margin-top:auto !important}.u-align-text--center{margin-left:auto !important;margin-right:auto !important;text-align:center !important}.u-align-text--left{margin-right:auto !important;text-align:left !important}.u-align-text--right{margin-left:auto !important;text-align:right !important}.u-align-text--small-to-default{padding-top:0.7rem}.u-align-text--x-small-to-default{padding-top:0.75rem}@media (prefers-reduced-motion: reduce){*{-webkit-animation:none !important;animation:none !important;transition:none !important}}.u-animation--spin{-webkit-animation:spin 1s infinite linear;animation:spin 1s infinite linear}@-webkit-keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.u-baseline-grid{position:relative}.u-baseline-grid::after{background:linear-gradient(to top, rgba(255,0,0,0.15), rgba(255,0,0,0.15) 1px, transparent 1px, transparent);background-size:100% .5rem;bottom:0;content:'';display:block;left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:200}html.u-baseline-grid{background-color:rgba(255,0,0,0.05);position:static}html.u-baseline-grid::after{z-index:-1}.u-baseline-grid__toggle{bottom:1.5rem;position:fixed;right:1.5rem;z-index:201}.u-embedded-media{height:0;margin-top:.5rem;max-width:100%;overflow:hidden;padding-bottom:56.25%;position:relative}.u-embedded-media__element{height:100%;left:0;position:absolute;top:0;width:100%}@media only screen and (min-width: 772px){.u-equal-height{display:flex}.u-equal-height.row{display:grid}}.u-float-right{float:right !important}@media (max-width: 620px){.u-float-right--small{float:right !important}}@media (min-width: 772px) and (max-width: 1036px){.u-float-right--medium{float:right !important}}@media (min-width: 1036px){.u-float-right--large{float:right !important}}.u-float-left{float:left !important}@media (max-width: 620px){.u-float-left--small{float:left !important}}@media (min-width: 772px) and (max-width: 1036px){.u-float-left--medium{float:left !important}}@media (min-width: 1036px){.u-float-left--large{float:left !important}}@media (min-width: 772px){.u-image-position{overflow:hidden;position:relative}.u-image-position .u-image-position--top,.u-image-position .u-image-position--bottom,.u-image-position .u-image-position--left,.u-image-position .u-image-position--right{margin:0;position:absolute}.u-image-position [class*='col-']{position:static}.u-image-position--top{top:0}.u-image-position--bottom{bottom:0}.u-image-position--left{left:0}.u-image-position--right{right:0}}.u-fixed-width .u-fixed-width,.row .u-fixed-width{margin-left:0;margin-right:0;padding-left:0;padding-right:0}.u-table-layout--auto{table-layout:auto !important}.u-no-margin{margin:0 !important}.u-no-margin--top{margin-top:0 !important}.u-no-margin--right{margin-right:0 !important}.u-no-margin--left{margin-left:0 !important}.u-table-cell-padding-overlap{margin-bottom:-.5rem !important;margin-top:calc(-.5rem - 1px) !important}.u-no-max-width{max-width:unset !important}.u-off-screen{height:1px !important;left:-10000px !important;overflow:hidden !important;position:absolute !important;top:auto !important;width:1px !important}.u-no-padding{padding:0 !important}.u-no-padding--top{padding-top:0 !important}.u-no-padding--right{padding-right:0 !important}.u-no-padding--bottom{padding-bottom:0 !important}.u-no-padding--left{padding-left:0 !important}.u-truncate{overflow:hidden !important;text-overflow:ellipsis !important;white-space:nowrap !important}.u-sv-3::after,.u-sv-2::after,.u-sv-1::after,.u-sv0::after,.u-sv1::after,.u-sv2::after,.u-sv3::after{content:'';display:block;height:1px;position:relative}.u-sv-3::after{margin-top:calc(-1.5rem - 1px)}.u-sv-2::after{margin-top:calc(-1rem - 1px)}.u-sv-1::after{margin-top:calc(-.5rem - 1px)}.u-sv0::after{margin-top:calc(0rem - 1px)}.u-sv1::after{margin-top:calc(.5rem - 1px)}.u-sv2::after{margin-top:calc(1rem - 1px)}.u-sv3::after{margin-top:calc(1.5rem - 1px)}.u-vertically-center{align-items:center !important;display:grid !important}.u-vertically-center>img{align-self:center !important}.u-hide{display:none !important}@media screen and (max-width: 771px){.u-hide--small{display:none !important}}@media (min-width: 772px) and (max-width: 1035px){.u-hide--medium{display:none !important}}@media screen and (min-width: 1036px){.u-hide--large{display:none !important}}td.u-hide,th.u-hide{display:table-cell !important;opacity:0 !important;overflow:hidden !important;padding:0 !important;white-space:nowrap !important;width:0 !important}@media screen and (max-width: 771px){td.u-hide--small,th.u-hide--small{display:table-cell !important;opacity:0 !important;overflow:hidden !important;padding:0 !important;white-space:nowrap !important;width:0 !important}}@media (min-width: 772px) and (max-width: 1035px){td.u-hide--medium,th.u-hide--medium{display:table-cell !important;opacity:0 !important;overflow:hidden !important;padding:0 !important;white-space:nowrap !important;width:0 !important}}@media screen and (min-width: 1036px){td.u-hide--large,th.u-hide--large{display:table-cell !important;opacity:0 !important;overflow:hidden !important;padding:0 !important;white-space:nowrap !important;width:0 !important}}.p-table-expanding .u-hide{display:none !important}@media screen and (max-width: 771px){.p-table-expanding .u-hide--small{display:none !important}}@media (min-width: 772px) and (max-width: 1035px){.p-table-expanding .u-hide--medium{display:none !important}}@media screen and (min-width: 1036px){.p-table-expanding .u-hide--large{display:none !important}}.u-show{display:inherit !important;display:initial !important}@media screen and (max-width: 772px){.u-show--small{display:inherit !important;display:initial !important}}@media (min-width: 772px) and (max-width: 1036px){.u-show--medium{display:inherit !important;display:initial !important}}@media screen and (min-width: 1036px){.u-show--large{display:inherit !important;display:initial !important}}.u-visualise-font-metrics{position:relative}.u-visualise-font-metrics::before{border-bottom-color:rgba(36,89,143,0.5);border-bottom-style:solid;border-top-color:rgba(14,134,32,0.5);border-top-style:solid;border-width:1px;content:'';height:calc(.176em);left:-2rem;position:absolute;top:calc(.239em - 1px);width:calc(4rem + 100%)}.u-visualise-font-metrics::after{background-color:rgba(199,22,43,0.5);content:'';height:1px;left:-2rem;position:absolute;top:calc(.932em - 1px);width:calc(4rem + 100%)}@media print{.u-no-print{display:none !important}} golang-github-canonical-candid-1.12.3/static/css/vanilla.css000066400000000000000000000026771457263123000237300ustar00rootroot00000000000000/* Import Vanilla framework: https://docs.vanillaframework.io/ */ @import "vanilla-framework-version-2.24.1.min.css"; html { overflow-y: hidden; height: 100%; } @media only screen and (min-width: 1681px) { html { font-size: 1rem !important; line-height: 1.5rem !important; } } body { overflow: hidden; overflow-y: scroll; height: 1vh; background: #f7f7f7; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; height: 100%; color: #333; font-family: Ubuntu, Arial, "libra sans", sans-serif; font-size: 16px; font-weight: 300; } .logo, .login-card { width: 90%; max-width: 439px; margin-left: auto; margin-right: auto; } .logo { text-align: center; } .logo__image { width: 132px; } .sso-buttons { margin-top: 1.5rem; display: flex; flex-direction: row; flex-wrap: wrap; justify-content: space-between; } .sso-button { border-color: #cdcdcd; padding-bottom: 1rem; padding-top: 1rem; width: 100%; height: 9rem; margin-right: 0 !important; display: flex; align-items: center; justify-content: center; } .sso-icon { margin-bottom: 0.5rem; } .sso-icon__image { height: 32px; width: auto; } .u-truncate--multiline { overflow: hidden !important; text-overflow: ellipsis !important; } @media only screen and (min-width: 620px) { .sso-buttons { overflow: hidden; } .sso-button { width: 195px; } .sso-button--wide { width: 100%; } } golang-github-canonical-candid-1.12.3/static/favicon.ico000066400000000000000000000024541457263123000231120ustar00rootroot00000000000000‰PNG  IHDR@@·ì pHYsHHFÉk> vpAg@@êóø`YPLTEÿÿÿw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)Rw)R=¨2rtRNS  "$)+,./02348=>?AEFGHKLMX[\]^_`bnpz{|}~‚ƒ‰ŠŽ“”•˜™Ÿ ¡¢°²³·¸¹ÀÂÃÄÐÒÔÕÖ×ÜÞßàâåæçéëìïñòóôõøùúûüýþÔžHbKGDˆH«IDATXí—W_AÀÿ‰]Ä4M4F,‰•ØQ *¢X°w# ÷ýòÀîମó4³³3¿ÝéP7Šî]ÜÆÝÅ^44XÇk $°tläÁñR DQ¼:tfØÂY¨ZA¼.ü`8ÂC¸ØWJ'î‚p?QZH¾ýÈ( Gíζ{6àyÌÁš±ïM&b±DòůbvòU;9—Nû|î Çíë[<ÉaîT½”¯9ȶv¤Ã•ËvuD²½sP“/_¹kqŸæ½vOôÎ?Ywv+s™ž¸ÅK4ÉOuOÎÎNwË7%¬[qOŽ‚)ËÆã7‡’Öíd¨9ã§qËOSÙò½æñm€žÍ|Ëoö¸5Oz³x./[êcvþÕ´\JúÜræ‚<»ù LÙGP*Ðz#é)ïOËÿw®ç œq]Òi¿P•üQÀ)Æ+.`TRQáÓnàwáD˜ܦ73_Ôc#ð½X*õ‚X(—&›jSŤj9I”2þ½ÀZñl^¼2/€Uià‹J=èV¾ éÖN`]EÁ:Ð)ÇC›@OÝàK«(HûÀ}*ˆ6F~Jð Ñ2BX`A ®¦ 6…üðñŸš‚‡à7• ¬  Eh€2rA®€¯ª ¾WôQçŽ~U?Ñ}ŸgÛÀª‚!`[àú/ж¶´ã@;µsA;µëvEÒ¯‰fUž{cUÖî yiZ¡3måv¦¼Þ¸RHþ]oÌêÎ]oëÎYóAë›æý å53Ò_»É~JÛ·¤÷CŸ Oiªsâ–㜨4©Î˜TõgeýiÝa_ØØPÞô7ýé¶6ý½ñ6×wØ­í=©²½ЄÿRfœVdoðw"zTXtSoftwarexÚsLÉOJUðÌMLO JML©/œÔ® ©MIEND®B`‚golang-github-canonical-candid-1.12.3/static/images/000077500000000000000000000000001457263123000222315ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/static/images/icons/000077500000000000000000000000001457263123000233445ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/static/images/icons/azure.svg000066400000000000000000000036121457263123000252150ustar00rootroot00000000000000 LOGO-AZURE Created with Sketch. golang-github-canonical-candid-1.12.3/static/images/icons/default.svg000066400000000000000000000035311457263123000255130ustar00rootroot00000000000000 LOGO-NONE Created with Sketch. golang-github-canonical-candid-1.12.3/static/images/icons/github.png000066400000000000000000000051011457263123000253310ustar00rootroot00000000000000‰PNG  IHDR@@ªiqÞtEXtSoftwareAdobe ImageReadyqÉe<$iTXtXML:com.adobe.xmp ŠéX³IDATxÚä[ilTU¾3ж‘.A\ TkkÝêþÀ£ þQ‰m„P£*’¸ðCL5QD­Á@QD‰ªâ㫉 IEI-¸`ÚP+¥¨»€’ÖïkÏ43ü¾wÞ¼™¾ 'ùrg¹÷Üs¾wß}÷Ü{^À$XòrróQ\\çc€L CªuÀŸÀÏ‚zàëÖ¶ƒ‰´/‡ƒ(®înÎSeð° ¨!=¾$Žç¢x˜ œ•  Ö ¬ª@D›/€ãtöQà^`„IŽª• ¢yH€ãÃP,žN3C#]Òÿ ⿤ç/G±(2þ=ÀlP—Pà8ë/ž†É¿r+¾"z='ΟŠâuàNãoy ˜º=#ÎBñ1pµI ÙÜ ÇM€8ÿPjRK8Ü`GBÀÆyÎMjÊ!¡KM€Lxï·›Ô–wVã)V­232‘?Õ…êŽÎ®Îo\}Nv_i6ʯx] LnKÂj«À÷€O€Ýç¨m6mŽ3 Ã(ØnKœO—H¬ÀFé.(,j{:ŠÀcÀhoV«Ño{T¿ß¡¸Ì¦ý¯@ Ú ÿ1Ö^êÀy#O†ÞA«QV÷Aùûð#ÀðöùZ¾rY Œò%tΖÿý­–G;&ÛP ¾UZŽ>Ey0h½ƒ%s‘ÄõMÊU'Ãè)\æÚ-qQ—èkÔòêCßoV#`©Cç)ì*ˆáunÆ»Öä•-"éâã}¡‚a,žÃ«ª°q˜fzM\2O|$@B[S9>" [yá–D ±ý,e§Wøˆ­-³Äç0 ÈU*¹ÊGhmÉŸ(W*àãk‘XöHu*åáܨlü$fé~ñ^l©T6ëó9€{¡HžýN¥˜ˆNúiÁ?FÈjoœ¢YqPYå7çeЦ—•ͦ~cs³#¿m¤H  šÁô>¿zÛx ü®hRtø„do Äÿ{u ‚ʰµ58¨¨{ ©hp<Ь2ƒŠè’•ŒRÔM*•LT6’€nEýñ)@@¾¢n7W‚­Š@ˆs@–Óc§!X òøîˆ±ß̘09ö+úHóY-W*œ§ì'ÊNnõ1·(ë7•¥Lò€ü6üiS™²YiÃÚñ.:J†”¹˜¤w$¹I»Âã¶òEƒ:&ùêó·ÞyA8Á¥£vëšçU>ºúU.œ¯£ï¡{ù#Þ æ÷ÁÕ_N[\4íó9 J.PFQá²XœìMyæ¿ÜãRElÞSÈd‚I.•ý< |¨IPré8mæ)ôJ`¢K5ßÂξ¤ü€ÌŒ fYÅJ†à!èà}à $Æbƒ¹¿L½z†­]í;^½óññà†²q¨[û~¸¤ƒ4™ÝÏŽªÌLÌi`¬AêoúÏçí¶Ò¸Àª•Ù™ÉÏŸFMâ,æp·ªØô'[óˆK®|'À–ãHÇ<4\£w ÐhƒÔã±3.œî&½Ìuz{È0ÕèÎ* ýXúMGÁ÷¦ÿ|>æÌÆë¤.×Ý;Läùb,ù‹(´;¢òY2"Çxè<ó. ]ýˆ9€‚û¢÷ÙOøXa¡`:þß‚zm@ >“Àël:]ƒ?ÐZ ýÇ ?Ïx››83zS÷„«‡ L‹«¶ÚAž ûÎÓ˜§m:݇Á[=t¾Z|‹«$©L¹¬3—@Ùî°ú“df棅óßüØðÊov:ùŰƒ+N/¶á÷ÉÐïpD€t^"÷xFŒ¿k ¬Ü$X$éêï8ÕðuœÉ°·ÞjK,¦HÎ=!q*$PÒö +çÝ Ž†[P,´øûM0Ãç,,Åi¶øbYwÇ’·çeyÙë£[à!Øc±jÞ˜b±Þs;$«¾Ã2²¸ÏÀ +ü¾DÀgüüТÍ3Ä ¾Wcté4£“H÷6ÊÐ_­Óª½=QÌôØ/}x¿Ó¦Rój„?O•ç~§§ œ£¦Šm* Ä9I‰â)Óÿ²¤•®±0ì€Ký9Æú´—.ßaZý-n}ðäÍQÊu©ì D'[æº}Ëz™Ùc’Û¬…èñˆ§ïˈ¨ÁX~Œœ§ÎB,ƒ´õ ­ã¹â'ÐÛÛkNf9é ø_€T{"€æU>MIEND®B`‚golang-github-canonical-candid-1.12.3/static/images/icons/google.svg000066400000000000000000000043571457263123000253520ustar00rootroot00000000000000 LOGO-GOOGLE-SSO Created with Sketch. golang-github-canonical-candid-1.12.3/static/images/icons/keystone.svg000066400000000000000000000041441457263123000257310ustar00rootroot00000000000000 LOGO-KEYSTONE Created with Sketch. golang-github-canonical-candid-1.12.3/static/images/icons/ldap.svg000066400000000000000000000025071457263123000250110ustar00rootroot00000000000000 LOGO-LDAP Created with Sketch. golang-github-canonical-candid-1.12.3/static/images/icons/openid.svg000066400000000000000000000032241457263123000253440ustar00rootroot00000000000000 LOGO-OPENID Created with Sketch. golang-github-canonical-candid-1.12.3/static/images/icons/static-domain.svg000066400000000000000000000036231457263123000266250ustar00rootroot00000000000000 LOGO-LOCAL AUTH Created with Sketch. golang-github-canonical-candid-1.12.3/static/images/icons/static.svg000066400000000000000000000055121457263123000253570ustar00rootroot00000000000000 LOGO-AGENT AUTH Created with Sketch. golang-github-canonical-candid-1.12.3/static/images/icons/usso.svg000066400000000000000000000003671457263123000250640ustar00rootroot00000000000000 golang-github-canonical-candid-1.12.3/static/images/logo-canonical-aubergine.svg000066400000000000000000000123001457263123000275720ustar00rootroot00000000000000 golang-github-canonical-candid-1.12.3/store/000077500000000000000000000000001457263123000206315ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/store/config.go000066400000000000000000000054321457263123000224310ustar00rootroot00000000000000package store import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/aclstore/v2" "github.com/juju/utils/v2/debugstatus" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/meeting" ) var backends = make(map[string]func(func(interface{}) error) (BackendFactory, error)) // Backend is the interface provided by a storage backend // implementation. Backend instances should be closed after use. type Backend interface { // ProviderDataStore returns a new ProviderDataStore // implementation that uses the backend. ProviderDataStore() ProviderDataStore // BakeryRootKeyStore returns a new bakery.RootKeyStore // implementation that uses the backend. BakeryRootKeyStore() bakery.RootKeyStore // MeetingStore returns a new meeting.Store implementation // that uses the backend. MeetingStore() meeting.Store // DebugStatusCheckerFuncs returns a set of // debugstatus.CheckerFuncs that can be used to provide a status // of the backend in the /debug/status endpoint. DebugStatusCheckerFuncs() []debugstatus.CheckerFunc // Store returns a new store.Store instance that uses // the backend. Store() Store // ACLStore returns a new aclstore.Store that is used to provide // ACLs for system functions. ACLStore() aclstore.ACLStore // Close closes the Backend instance. Close() } // BackendFactory represents a value that can create new storage // backend instances. type BackendFactory interface { NewBackend() (Backend, error) } // Register is used by storage backends to register a function // that can be used to unmarshal parameters for a storage backend. When // a storage backend with the given type is used, f will be called to // unmarshal its parameters from YAML. Its argument will be an // unmarshalYAML function that can be used to unmarshal the // configuration parameters into its argument according to the rules // specified in gopkg.in/yaml.v2, and it should return a function that // can be used to create a storage backend. func Register(storageType string, f func(func(interface{}) error) (BackendFactory, error)) { backends[storageType] = f } // Config allows a storage instance to be unmarshaled from a YAML // configuration file. The "type" field determines which registered // backend is used for the unmarshaling. type Config struct { BackendFactory } func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { var t struct { Type string } if err := unmarshal(&t); err != nil { return errgo.Notef(err, "cannot unmarshal storage") } if storageUnmarshaler, ok := backends[t.Type]; ok { bf, err := storageUnmarshaler(unmarshal) if err != nil { return errgo.Notef(err, "cannot unmarshal %s configuration", t.Type) } c.BackendFactory = bf return err } return errgo.Newf("unrecognised storage backend type %q", t.Type) } golang-github-canonical-candid-1.12.3/store/error.go000066400000000000000000000033461457263123000223170ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package store import ( "fmt" errgo "gopkg.in/errgo.v1" ) var ( // ErrNotFound is the error cause used when an identity cannot be // found in storage. ErrNotFound = errgo.New("not found") // ErrDuplicateUsername is the error cause used when an update // attempts to set a username that is already in use. ErrDuplicateUsername = errgo.New("duplicate username") // ErrDuplicateCredential is the error cause used when we try to // store user credential with a name that is already in use. ErrDuplicateCredential = errgo.New("duplicate credential name") ) // NotFoundError creates a new error with a cause of ErrNotFound and an // appropriate message. func NotFoundError(id string, providerID ProviderIdentity, username string) error { msg := "identity not specified" switch { case id != "": msg = fmt.Sprintf("identity %q not found", id) case providerID != "": msg = fmt.Sprintf("identity %q not found", providerID) case username != "": msg = fmt.Sprintf("user %s not found", username) } err := errgo.WithCausef(nil, ErrNotFound, msg) err.(*errgo.Err).SetLocation(1) return err } // DuplicateUsernameError creates a new error with a cause of // ErrDuplicateUsername and an appropriate message. func DuplicateUsernameError(username string) error { err := errgo.WithCausef(nil, ErrDuplicateUsername, "username %s already in use", username) err.(*errgo.Err).SetLocation(1) return err } // KeyNotFoundError creates a new error with a cause of ErrNotFound and // an appropriate message. func KeyNotFoundError(key string) error { err := errgo.WithCausef(nil, ErrNotFound, "key %s not found", key) err.(*errgo.Err).SetLocation(1) return err } golang-github-canonical-candid-1.12.3/store/error_test.go000066400000000000000000000023361457263123000233540ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package store_test import ( "testing" qt "github.com/frankban/quicktest" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/store" ) func TestNotFoundError(t *testing.T) { c := qt.New(t) err := store.NotFoundError("1234", "", "") c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) c.Assert(err, qt.ErrorMatches, `identity "1234" not found`) err = store.NotFoundError("", store.MakeProviderIdentity("test", "test-user"), "") c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) c.Assert(err, qt.ErrorMatches, `identity "test:test-user" not found`) err = store.NotFoundError("", "", "test-user") c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) c.Assert(err, qt.ErrorMatches, `user test-user not found`) err = store.NotFoundError("", "", "") c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) c.Assert(err, qt.ErrorMatches, `identity not specified`) } func TestDuplicateUsernameError(t *testing.T) { c := qt.New(t) err := store.DuplicateUsernameError("test-user") c.Assert(errgo.Cause(err), qt.Equals, store.ErrDuplicateUsername) c.Assert(err, qt.ErrorMatches, `username test-user already in use`) } golang-github-canonical-candid-1.12.3/store/keyvalue.go000066400000000000000000000010641457263123000230060ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package store import ( "context" "github.com/juju/simplekv" ) // An ProviderDataStore is a data store that supports identity provider // specific KeyValueStores. These stores can be used by identity // providers to store data that is not directly related to an identity. type ProviderDataStore interface { // KeyValueStore gets a key-value store for use by the given // identity provider. KeyValueStore(ctx context.Context, idp string) (simplekv.Store, error) } golang-github-canonical-candid-1.12.3/store/memstore/000077500000000000000000000000001457263123000224645ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/store/memstore/config.go000066400000000000000000000033201457263123000242560ustar00rootroot00000000000000package memstore import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/aclstore/v2" "github.com/juju/simplekv/memsimplekv" "github.com/juju/utils/v2/debugstatus" "github.com/canonical/candid/meeting" "github.com/canonical/candid/store" ) func init() { store.Register("memory", func(func(interface{}) error) (store.BackendFactory, error) { return &backend{ store: NewStore(), rootKeys: bakery.NewMemRootKeyStore(), providerData: NewProviderDataStore(), meetingStore: NewMeetingStore(), aclStore: aclstore.NewACLStore(memsimplekv.NewStore()), }, nil }) } type backend struct { store store.Store providerData store.ProviderDataStore rootKeys bakery.RootKeyStore meetingStore meeting.Store aclStore aclstore.ACLStore } // NewBackend implements store.BackendFactory.NewBackend. func (b *backend) NewBackend() (store.Backend, error) { return b, nil } // ProviderDataStore implements store.Backend.ProviderDataStore. func (b *backend) ProviderDataStore() store.ProviderDataStore { return b.providerData } // Store implements store.Backend.Store. func (b *backend) Store() store.Store { return b.store } // BakeryRootKeyStore implements store.Backend.BakeryRootKeyStore. func (b *backend) BakeryRootKeyStore() bakery.RootKeyStore { return b.rootKeys } // DebugStatusCheckerFuncs implements store.Backend.DebugStatusCheckerFuncs. func (b *backend) DebugStatusCheckerFuncs() []debugstatus.CheckerFunc { return nil } // MeetingStore implements store.Backend.MeetingStore. func (b *backend) MeetingStore() meeting.Store { return b.meetingStore } func (b *backend) ACLStore() aclstore.ACLStore { return b.aclStore } func (b *backend) Close() { } golang-github-canonical-candid-1.12.3/store/memstore/credential.go000066400000000000000000000045351457263123000251340ustar00rootroot00000000000000// Copyright 2021 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package memstore import ( "bytes" "context" "fmt" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/store" ) func mfaCredentialKey(cred store.MFACredential) string { return fmt.Sprintf("%s-%s", cred.ProviderID, cred.Name) } // AddMFACredential stores the specified multi-factor credential. func (s *memStore) AddMFACredential(ctx context.Context, cred store.MFACredential) error { s.mu.Lock() defer s.mu.Unlock() key := mfaCredentialKey(cred) _, ok := s.credentials[key] if ok { return errgo.WithCausef(nil, store.ErrDuplicateCredential, "credential with name %q already exists", cred.Name) } s.credentials[key] = cred return nil } // RemoveMFACredential removes the multi-factor credential with the // specified username and credential name. func (s *memStore) RemoveMFACredential(ctx context.Context, providerID, name string) error { s.mu.Lock() defer s.mu.Unlock() key := mfaCredentialKey(store.MFACredential{ ProviderID: store.ProviderIdentity(providerID), Name: name, }) delete(s.credentials, key) return nil } // ClearMFACredentials removes all multi-factor credentials for the specified user. func (s *memStore) ClearMFACredentials(ctx context.Context, providerID string) error { s.mu.Lock() defer s.mu.Unlock() keys := []string{} for key, cred := range s.credentials { if string(cred.ProviderID) == providerID { keys = append(keys, key) } } for _, key := range keys { delete(s.credentials, key) } return nil } // UserMFACredentials returns all multi-factor credentials for the specified user. func (s *memStore) UserMFACredentials(ctx context.Context, providerID string) ([]store.MFACredential, error) { s.mu.Lock() defer s.mu.Unlock() var credentials []store.MFACredential for _, cred := range s.credentials { if string(cred.ProviderID) == providerID { credentials = append(credentials, cred) } } return credentials, nil } // IncrementMFACredentialSignCount increments the multi-factor credential sign count. func (s *memStore) IncrementMFACredentialSignCount(ctx context.Context, credentialID []byte) error { s.mu.Lock() defer s.mu.Unlock() for key, c := range s.credentials { if bytes.Compare(c.ID, credentialID) == 0 { c.AuthenticatorSignCount++ s.credentials[key] = c break } } return nil } golang-github-canonical-candid-1.12.3/store/memstore/export_test.go000066400000000000000000000005241457263123000253740ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package memstore import ( "context" "time" "github.com/canonical/candid/meeting" ) var PutAtTime = func(ctx context.Context, s meeting.Store, id, address string, now time.Time) error { return s.(*meetingStore).put(ctx, id, address, now) } golang-github-canonical-candid-1.12.3/store/memstore/keyvalue.go000066400000000000000000000015121457263123000246370ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package memstore import ( "context" "sync" "github.com/juju/simplekv" "github.com/juju/simplekv/memsimplekv" "github.com/canonical/candid/store" ) // NewProviderDataStore creates a new in-memory store.ProviderDataStore. func NewProviderDataStore() store.ProviderDataStore { return &providerDataStore{ stores: make(map[string]simplekv.Store), } } type providerDataStore struct { mu sync.Mutex stores map[string]simplekv.Store } // KeyValueStore implements store.ProviderDataStore.KeyValueStore. func (s *providerDataStore) KeyValueStore(_ context.Context, idp string) (simplekv.Store, error) { s.mu.Lock() defer s.mu.Unlock() if s.stores[idp] == nil { s.stores[idp] = memsimplekv.NewStore() } return s.stores[idp], nil } golang-github-canonical-candid-1.12.3/store/memstore/meeting.go000066400000000000000000000042651457263123000244520ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package memstore import ( "context" "sync" "time" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/meeting" ) // NewMeetingStore creates a new in-memory meeting.Store implementation. func NewMeetingStore() meeting.Store { return &meetingStore{ data: make(map[string]meetingStoreEntry), } } type meetingStore struct { mu sync.Mutex data map[string]meetingStoreEntry } type meetingStoreEntry struct { address string time time.Time } // Context implements meeting.Store.Context by returning the given // context unchanged along with a NOP close function. func (s *meetingStore) Context(ctx context.Context) (_ context.Context, close func()) { return ctx, func() {} } // Put implements meeting.Store.Put. func (s *meetingStore) Put(ctx context.Context, id, address string) error { return errgo.Mask(s.put(ctx, id, address, time.Now())) } // put is the internal version of Put which takes a time // for testing purposes. func (s *meetingStore) put(_ context.Context, id, address string, now time.Time) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.data[id]; ok { return errgo.Newf("duplicate id %q in meeting store", id) } s.data[id] = meetingStoreEntry{ address: address, time: now, } return nil } // Get implements meeting.Store.Get. func (s *meetingStore) Get(_ context.Context, id string) (address string, _ error) { s.mu.Lock() defer s.mu.Unlock() if e, ok := s.data[id]; ok { return e.address, nil } return "", errgo.New("rendezvous not found, probably expired") } // Remove implements meeting.Store.Remove. func (s *meetingStore) Remove(_ context.Context, id string) (time.Time, error) { s.mu.Lock() defer s.mu.Unlock() e := s.data[id] delete(s.data, id) return e.time, nil } // RemoveOld implements meeting.Store.RemoveOld. func (s *meetingStore) RemoveOld(_ context.Context, addr string, olderThan time.Time) (ids []string, err error) { s.mu.Lock() defer s.mu.Unlock() for k, v := range s.data { if addr != "" && v.address != addr { continue } if v.time.Before(olderThan) { delete(s.data, k) ids = append(ids, k) } } return ids, nil } golang-github-canonical-candid-1.12.3/store/memstore/memstore_test.go000066400000000000000000000022431457263123000257060ustar00rootroot00000000000000package memstore_test import ( "testing" qt "github.com/frankban/quicktest" "github.com/juju/aclstore/v2" "github.com/juju/simplekv/memsimplekv" "github.com/canonical/candid/meeting" "github.com/canonical/candid/store" "github.com/canonical/candid/store/memstore" "github.com/canonical/candid/store/storetest" ) func TestKeyValueStore(t *testing.T) { c := qt.New(t) defer c.Done() storetest.TestKeyValueStore(c, func(c *qt.C) store.ProviderDataStore { return memstore.NewProviderDataStore() }) } func TestStore(t *testing.T) { c := qt.New(t) defer c.Done() storetest.TestStore(c, func(c *qt.C) store.Store { return memstore.NewStore() }) } func TestMeetingStore(t *testing.T) { c := qt.New(t) defer c.Done() storetest.TestMeetingStore(c, func(c *qt.C) meeting.Store { return memstore.NewMeetingStore() }, memstore.PutAtTime) } func TestACLStore(t *testing.T) { c := qt.New(t) defer c.Done() storetest.TestACLStore(c, func(c *qt.C) aclstore.ACLStore { return aclstore.NewACLStore(memsimplekv.NewStore()) }) } func TestConfigUnmarshal(t *testing.T) { c := qt.New(t) defer c.Done() storetest.TestUnmarshal(c, ` storage: type: memory `) } golang-github-canonical-candid-1.12.3/store/memstore/store.go000066400000000000000000000302051457263123000241470ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package memstore provides an in-memory implementation of the store. // This might be useful for simple test systems. package memstore import ( "context" "fmt" "sort" "strconv" "strings" "sync" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/store" ) type memStore struct { mu sync.Mutex identities []*store.Identity credentials map[string]store.MFACredential } // NewStore creates a new in-memory store.Store instance. func NewStore() store.Store { return &memStore{ credentials: make(map[string]store.MFACredential), } } // Context implements store.Store.Context by returning the given context // and a NOP close function. func (s *memStore) Context(ctx context.Context) (_ context.Context, cancel func()) { return ctx, func() {} } var adminID = store.MakeProviderIdentity("idm", "admin") // RemoveAll is implemented so that tests can clear out the data. // It removes all identities except the admin identity created at // init time. // TODO provide a standard store.Store way of removing // identities. func (s *memStore) RemoveAll() { s.mu.Lock() defer s.mu.Unlock() var identities []*store.Identity for _, identity := range s.identities { if identity.ProviderID == adminID { identities = append(identities, identity) } } s.identities = identities } // Identity implements store.Store.Identity. func (s *memStore) Identity(_ context.Context, identity *store.Identity) error { s.mu.Lock() defer s.mu.Unlock() var id *store.Identity switch { case identity.ID != "": n, err := strconv.Atoi(identity.ID) if err != nil || n >= len(s.identities) { return store.NotFoundError(identity.ID, "", "") } id = s.identities[n] case identity.ProviderID != "": id = s.identityFromProviderID(identity.ProviderID) if id == nil { return store.NotFoundError("", identity.ProviderID, "") } case identity.Username != "": id = s.identityFromUsername(identity.Username) if id == nil { return store.NotFoundError("", "", identity.Username) } default: return store.NotFoundError("", "", "") } copyIdentity(identity, id) return nil } // identityFromProviderID performs a linear search to find an identitty // with the given providerID. func (s *memStore) identityFromProviderID(providerID store.ProviderIdentity) *store.Identity { for _, id := range s.identities { if id.ProviderID == providerID { return id } } return nil } // identityFromUsername performs a linear search to find an identitty // with the given username. func (s *memStore) identityFromUsername(username string) *store.Identity { for _, id := range s.identities { if id.Username == username { return id } } return nil } // FindIdentities implements store.Store.FindIdentities. func (s *memStore) FindIdentities(ctx context.Context, ref *store.Identity, filter store.Filter, sortFields []store.Sort, skip, limit int) ([]store.Identity, error) { s.mu.Lock() defer s.mu.Unlock() identities := make([]store.Identity, 0, len(s.identities)) for _, identity := range s.identities { if !matchIdentity(identity, ref, filter) { continue } var identity1 store.Identity copyIdentity(&identity1, identity) identities = append(identities, identity1) } if skip > len(identities) { return nil, nil } if len(sortFields) > 0 { sort.Sort(identitySort{ identities: identities, sort: sortFields, }) } identities = identities[skip:] if limit > 0 && limit < len(identities) { identities = identities[:limit] } return identities, nil } func matchIdentity(a, b *store.Identity, filter store.Filter) bool { for f, c := range filter { if c == store.NoComparison { continue } var r int switch store.Field(f) { case store.ProviderID: r = strings.Compare(string(a.ProviderID), string(b.ProviderID)) case store.Username: r = strings.Compare(a.Username, b.Username) case store.Name: r = strings.Compare(a.Name, b.Name) case store.Email: r = strings.Compare(a.Email, b.Email) case store.LastLogin: r = cmpTime(a.LastLogin, b.LastLogin) case store.LastDischarge: r = cmpTime(a.LastDischarge, b.LastDischarge) case store.Owner: r = strings.Compare(string(a.Owner), string(b.Owner)) default: panic("unsupported filter field") } if !matchCmp(r, c) { return false } } return true } // matchCmp determines whether the given value n which is a result of a // "cmp" function such as strings.Compare indicates that the compared // values have the relationship specified by the given store.Comparison. func matchCmp(n int, c store.Comparison) bool { switch c { case store.Equal: return n == 0 case store.NotEqual: return n != 0 case store.GreaterThan: return n > 0 case store.LessThan: return n < 0 case store.GreaterThanOrEqual: return n >= 0 case store.LessThanOrEqual: return n <= 0 default: panic("unsupported comparison") } } func cmpTime(t, u time.Time) int { if t.After(u) { return 1 } if t.Before(u) { return -1 } return 0 } type identitySort struct { identities []store.Identity sort []store.Sort } func (s identitySort) Len() int { return len(s.identities) } func (s identitySort) Swap(i, j int) { s.identities[i], s.identities[j] = s.identities[j], s.identities[i] } func (s identitySort) Less(i, j int) bool { a := &s.identities[i] b := &s.identities[j] for _, sort := range s.sort { switch s.cmp(a, b, sort.Field, sort.Descending) { case 1: return false case -1: return true } } return false } func (s identitySort) cmp(a, b *store.Identity, f store.Field, desc bool) int { cmp := 0 switch f { case store.ProviderID: cmp = strings.Compare(string(a.ProviderID), string(b.ProviderID)) case store.Username: cmp = strings.Compare(a.Username, b.Username) case store.Name: cmp = strings.Compare(a.Name, b.Name) case store.Email: cmp = strings.Compare(a.Email, b.Email) case store.LastLogin: cmp = cmpTime(a.LastLogin, b.LastLogin) case store.LastDischarge: cmp = cmpTime(a.LastDischarge, b.LastDischarge) default: panic("unsupported sort field") } if desc { return 0 - cmp } return cmp } // UpdateIdentity implements store.Store.UpdateIdentity. func (s *memStore) UpdateIdentity(_ context.Context, identity *store.Identity, update store.Update) error { s.mu.Lock() defer s.mu.Unlock() var id *store.Identity switch { case identity.ID != "": n, err := strconv.Atoi(identity.ID) if err != nil || n >= len(s.identities) { return store.NotFoundError(identity.ID, "", "") } id = s.identities[n] case identity.ProviderID != "": id = s.identityFromProviderID(identity.ProviderID) if id == nil { if identity.Username == "" || update[store.Username] == store.NoUpdate { return store.NotFoundError("", identity.ProviderID, "") } n := len(s.identities) id = &store.Identity{ ID: fmt.Sprintf("%d", n), ProviderID: identity.ProviderID, ProviderInfo: make(map[string][]string), ExtraInfo: make(map[string][]string), } if err := s.updateIdentity(id, identity, update); err != nil { return errgo.Mask(err, errgo.Is(store.ErrDuplicateUsername)) } s.identities = append(s.identities, id) identity.ID = id.ID return nil } case identity.Username != "": id = s.identityFromUsername(identity.Username) if id == nil { return store.NotFoundError("", "", identity.Username) } default: return store.NotFoundError("", "", "") } return errgo.Mask(s.updateIdentity(id, identity, update), errgo.Is(store.ErrDuplicateUsername)) } func (s *memStore) updateIdentity(dst, src *store.Identity, update store.Update) error { if update[store.ProviderID] != store.NoUpdate { panic(errgo.Newf("unsupported operation %v requested on ProviderID field", update[store.ProviderID])) } switch update[store.Username] { case store.NoUpdate: case store.Set: id := s.identityFromUsername(src.Username) if id != nil && id != dst { return store.DuplicateUsernameError(src.Username) } dst.Username = src.Username default: panic("unsupported operation requested on Username field") } dst.Name = updateString(dst.Name, src.Name, update[store.Name]) dst.Email = updateString(dst.Email, src.Email, update[store.Email]) dst.Groups = updateStrings(dst.Groups, src.Groups, update[store.Groups]) dst.PublicKeys = updateKeys(dst.PublicKeys, src.PublicKeys, update[store.PublicKeys]) dst.LastDischarge = updateTime(dst.LastDischarge, src.LastDischarge, update[store.LastDischarge]) dst.LastLogin = updateTime(dst.LastLogin, src.LastLogin, update[store.LastLogin]) dst.ProviderInfo = updateMap(dst.ProviderInfo, src.ProviderInfo, update[store.ProviderInfo]) dst.ExtraInfo = updateMap(dst.ExtraInfo, src.ExtraInfo, update[store.ExtraInfo]) dst.Owner = updateProviderIdentity(dst.Owner, src.Owner, update[store.Owner]) return nil } func updateString(dst, src string, op store.Operation) string { switch op { case store.NoUpdate: return dst case store.Set: return src case store.Clear: return "" default: panic("unsupported operation requested on string field") } } func updateProviderIdentity(dst, src store.ProviderIdentity, op store.Operation) store.ProviderIdentity { switch op { case store.NoUpdate: return dst case store.Set: return src case store.Clear: return "" default: panic("unsupported operation requested on store.ProviderIdentity field") } } func updateMFACredentials(dst, src []store.MFACredential, op store.Operation) []store.MFACredential { switch op { case store.NoUpdate: return dst case store.Set: return src case store.Clear: return nil default: panic("unsupported operation requested on store.ProviderIdentity field") } } func updateTime(dst, src time.Time, op store.Operation) time.Time { switch op { case store.NoUpdate: return dst case store.Set: return src case store.Clear: return time.Time{} default: panic("unsupported operation requested on string field") } } func updateStrings(dst, src []string, op store.Operation) []string { switch op { case store.NoUpdate: return dst case store.Set: return append([]string(nil), src...) case store.Clear: return nil case store.Push: for _, s := range src { if !containsString(dst, s) { dst = append(dst, s) } } return dst case store.Pull: var ndst []string for _, s := range dst { if !containsString(src, s) { ndst = append(ndst, s) } } return ndst default: panic("unsupported operation requested on []string field") } } func containsString(ss []string, s string) bool { for _, t := range ss { if s == t { return true } } return false } func updateKeys(dst, src []bakery.PublicKey, op store.Operation) []bakery.PublicKey { switch op { case store.NoUpdate: return dst case store.Set: return append([]bakery.PublicKey(nil), src...) case store.Clear: return nil case store.Push: for _, k := range src { if !containsKey(dst, k) { dst = append(dst, k) } } return dst case store.Pull: var ndst []bakery.PublicKey for _, k := range dst { if !containsKey(src, k) { ndst = append(ndst, k) } } return ndst default: panic("unsupported operation requested on []bakery.PublicKey field") } } func containsKey(ks []bakery.PublicKey, k bakery.PublicKey) bool { for _, k1 := range ks { if k == k1 { return true } } return false } func updateMap(dst, src map[string][]string, op store.Operation) map[string][]string { for k, v := range src { ss := updateStrings(dst[k], v, op) if len(ss) == 0 { delete(dst, k) } else { dst[k] = ss } } return dst } func copyIdentity(dst, src *store.Identity) { *dst = *src dst.Groups = updateStrings(nil, src.Groups, store.Set) dst.PublicKeys = updateKeys(nil, src.PublicKeys, store.Set) dst.ProviderInfo = updateMap(make(map[string][]string), src.ProviderInfo, store.Set) dst.ExtraInfo = updateMap(make(map[string][]string), src.ExtraInfo, store.Set) } // IdentityCounts implements store.Store.IdentityCounts. func (s *memStore) IdentityCounts(_ context.Context) (map[string]int, error) { s.mu.Lock() defer s.mu.Unlock() counts := make(map[string]int) for _, id := range s.identities { counts[id.ProviderID.Provider()]++ } return counts, nil } golang-github-canonical-candid-1.12.3/store/mgostore/000077500000000000000000000000001457263123000224705ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/store/mgostore/backend.go000066400000000000000000000113741457263123000244140ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package mgostore import ( "context" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/mgorootkeystore" "github.com/juju/aclstore/v2" mgo "github.com/juju/mgo/v2" "github.com/juju/simplekv/mgosimplekv" "github.com/juju/utils/v2/debugstatus" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/meeting" "github.com/canonical/candid/store" ) const aclsCollection = "acls" // backend provides a wrapper around a single mongodb database that // can be used as the persistent storage for the various types of store // required by the identity service. type backend struct { db *mgo.Database rootKeys *mgorootkeystore.RootKeys aclStore aclstore.ACLStore } // NewBackend creates a new Backend instance using the given // *mgo.Database. The given Database's underlying session will be // copied. The Backend must be closed when finished with. func NewBackend(db *mgo.Database) (_ store.Backend, err error) { db = db.With(db.Session.Copy()) defer func() { if err != nil { db.Session.Close() } }() if err := ensureIdentityIndexes(db); err != nil { return nil, errgo.Mask(err) } if err := ensureCredentialsIndexes(db); err != nil { return nil, errgo.Mask(err) } if err := ensureMeetingIndexes(db); err != nil { return nil, errgo.Mask(err) } rk := mgorootkeystore.NewRootKeys(1000) // TODO(mhilton) make this configurable? if err := ensureBakeryIndexes(rk, db); err != nil { return nil, errgo.Mask(err) } aclStore, err := mgosimplekv.NewStore(db.C(aclsCollection)) if err != nil { return nil, errgo.Mask(err) } return &backend{ db: db, rootKeys: rk, aclStore: aclstore.NewACLStore(aclStore), }, nil } // Close cleans up resources associated with the database. func (b *backend) Close() { b.db.Session.Close() } // context returns a context with session information attached such that // subsequent operations that use the context will be consistent. This // function may return the context it was passed if suitable session // information is already available. The return close function should // always be called once the context is not longer needed. func (b *backend) context(ctx context.Context) (_ context.Context, close func()) { if s, _ := ctx.Value(sessionKey{}).(*mgo.Session); s != nil { return ctx, func() {} } // TODO (mhilton) add some more advanced session pooling. s := b.db.Session.Copy() return context.WithValue(ctx, sessionKey{}, s), s.Close } type sessionKey struct{} // s returns a *mgo.Session for use in subsequent queries. The returned // session must be closed once finished with. func (b *backend) s(ctx context.Context) *mgo.Session { if s, _ := ctx.Value(sessionKey{}).(*mgo.Session); s != nil { return s.Clone() } return b.db.Session.Copy() } // c returns a *mgo.Collection with the given name in the current // database. The collection's underlying session must be closed when the // query is complete. func (b *backend) c(ctx context.Context, name string) *mgo.Collection { return b.db.C(name).With(b.s(ctx)) } // Store implements store.Backend.Store. func (b *backend) Store() store.Store { return &identityStore{b} } // MeetingStore implements store.Backend.MeetingStore. func (b *backend) MeetingStore() meeting.Store { return &meetingStore{b} } // BakeryRootKeyStore implements store.Backend.BakeryRootKeyStore. func (b *backend) BakeryRootKeyStore() bakery.RootKeyStore { return &rootKeyStore{ b: b, policy: mgorootkeystore.Policy{ ExpiryDuration: 365 * 24 * time.Hour, }, } } // ProviderDataStore implements store.Backend.ProviderDataStore. func (b *backend) ProviderDataStore() store.ProviderDataStore { return &providerDataStore{b} } // DebugStatusCheckerFuncs implements store.Backend.DebugStatusCheckerFuncs. func (b *backend) DebugStatusCheckerFuncs() []debugstatus.CheckerFunc { return []debugstatus.CheckerFunc{ debugstatus.MongoCollections(collector{b.db}), b.meetingStatus, } } // ACLStore implements store.Backend.ACLStore. func (b *backend) ACLStore() aclstore.ACLStore { return b.aclStore } type collector struct { db *mgo.Database } // Collections implements debugstatus.Collector.Collections. func (c collector) Collections() []*mgo.Collection { return []*mgo.Collection{ c.db.C(macaroonCollection), c.db.C(meetingCollection), c.db.C(identitiesCollection), c.db.C(aclsCollection), c.db.C(credentialCollection), } } // CollectionNames implements debugstatus.Collector.CollectionNames by // wrapping the CollectionNames method of mgo.Database with some session // handling code. func (c collector) CollectionNames() ([]string, error) { s := c.db.Session.Copy() defer s.Close() return c.db.With(s).CollectionNames() } golang-github-canonical-candid-1.12.3/store/mgostore/backend_test.go000066400000000000000000000017551457263123000254550ustar00rootroot00000000000000package mgostore_test import ( "context" "testing" "time" qt "github.com/frankban/quicktest" "github.com/juju/mgotest" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/store" "github.com/canonical/candid/store/mgostore" ) func TestNewBackend(t *testing.T) { c := qt.New(t) defer c.Done() db, err := mgotest.New() if errgo.Cause(err) == mgotest.ErrDisabled { c.Skip("mgotest disabled") } c.Assert(err, qt.IsNil) defer db.Close() // mgotest sets the SocketTimout to 1s. Restore it back to the // default value. db.Session.SetSocketTimeout(time.Minute) testdb := db.Database s := testdb.Session.Copy() testdb = testdb.With(s) backend, err := mgostore.NewBackend(testdb) c.Assert(err, qt.IsNil) c.Defer(backend.Close) s.Close() ctx := context.Background() _, err = backend.Store().FindIdentities(ctx, &store.Identity{}, store.Filter{}, nil, 0, 0) c.Assert(err, qt.IsNil) err = backend.ACLStore().CreateACL(ctx, "test", []string{"test"}) c.Assert(err, qt.IsNil) } golang-github-canonical-candid-1.12.3/store/mgostore/bakery.go000066400000000000000000000024241457263123000242760ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package mgostore import ( "context" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/mgorootkeystore" mgo "github.com/juju/mgo/v2" errgo "gopkg.in/errgo.v1" ) const macaroonCollection = "macaroons" type rootKeyStore struct { b *backend policy mgorootkeystore.Policy } // Get implements bakery.RootKeyStore.Get by wrapping mgorootkeystore // implementation with code to determine the collection. func (s rootKeyStore) Get(ctx context.Context, id []byte) ([]byte, error) { coll := s.b.c(ctx, macaroonCollection) defer coll.Database.Session.Close() store := s.b.rootKeys.NewStore(coll, s.policy) return store.Get(ctx, id) } // RootKey implements bakery.RootKeyStore.RootKey by wrapping // mgorootkeystore implementation with code to determine the collection. func (s rootKeyStore) RootKey(ctx context.Context) ([]byte, []byte, error) { coll := s.b.c(ctx, macaroonCollection) defer coll.Database.Session.Close() store := s.b.rootKeys.NewStore(coll, s.policy) return store.RootKey(ctx) } func ensureBakeryIndexes(rk *mgorootkeystore.RootKeys, db *mgo.Database) error { if err := rk.EnsureIndex(db.C(macaroonCollection)); err != nil { return errgo.Mask(err) } return nil } golang-github-canonical-candid-1.12.3/store/mgostore/config.go000066400000000000000000000023151457263123000242650ustar00rootroot00000000000000package mgostore import ( errgo "gopkg.in/errgo.v1" mgo "github.com/juju/mgo/v2" "github.com/canonical/candid/store" ) // Params holds the specification for the parameters // used in the config file. type Params struct { // Address holds the address of the MongoDB // server to connect to, in host:port form. Address string `yaml:"address"` // Database holds the database name to use. // If this is empty, "candid" will be used. Database string `yaml:"database"` } func init() { store.Register("mongodb", unmarshalBackend) } func unmarshalBackend(unmarshal func(interface{}) error) (store.BackendFactory, error) { var p Params if err := unmarshal(&p); err != nil { return nil, errgo.Mask(err) } if p.Address == "" { return nil, errgo.Newf("no address field in mongodb storage configuration") } if p.Database == "" { p.Database = "candid" } return p, nil } // NewBackend implements store.BackendFactory. func (p Params) NewBackend() (store.Backend, error) { logger.Infof("connecting to mongo") session, err := mgo.Dial(p.Address) if err != nil { return nil, errgo.Notef(err, "cannot dial mongo at %q", p.Address) } defer session.Close() db := session.DB(p.Database) return NewBackend(db) } golang-github-canonical-candid-1.12.3/store/mgostore/config_test.go000066400000000000000000000023461457263123000253300ustar00rootroot00000000000000package mgostore_test import ( "testing" qt "github.com/frankban/quicktest" "gopkg.in/yaml.v2" "github.com/canonical/candid/store" "github.com/canonical/candid/store/mgostore" "github.com/canonical/candid/store/storetest" ) func TestUnmarshal(t *testing.T) { c := qt.New(t) defer c.Done() f := newFixture(c) storetest.TestUnmarshal(c, ` storage: type: mongodb address: `+f.connStr+` database: `+f.db.Name+` `) } func TestUnmarshalWithNoAddress(t *testing.T) { c := qt.New(t) defer c.Done() configData := ` storage: type: mongodb ` var cfg struct { Storage *store.Config `yaml:"storage"` } err := yaml.Unmarshal([]byte(configData), &cfg) c.Assert(err, qt.ErrorMatches, `cannot unmarshal mongodb configuration: no address field in mongodb storage configuration`) } func TestUnmarshalWithoutDatabase(t *testing.T) { c := qt.New(t) defer c.Done() f := newFixture(c) configData := ` storage: type: mongodb address: ` + f.connStr + ` ` var cfg struct { Storage *store.Config `yaml:"storage"` } err := yaml.Unmarshal([]byte(configData), &cfg) c.Assert(err, qt.IsNil) p, ok := cfg.Storage.BackendFactory.(mgostore.Params) c.Assert(ok, qt.Equals, true) c.Assert(p.Database, qt.Equals, "candid") } golang-github-canonical-candid-1.12.3/store/mgostore/credential.go000066400000000000000000000101721457263123000251320ustar00rootroot00000000000000// Copyright 2021 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package mgostore import ( "context" "github.com/canonical/candid/store" "gopkg.in/errgo.v1" "github.com/juju/mgo/v2" "github.com/juju/mgo/v2/bson" ) type credentialDoc struct { Id []byte `bson:"_id"` ProviderID store.ProviderIdentity `bson:"provider-id"` Name string `bson:"name"` PublicKey []byte `bson:"public-key"` AttestationType string `bson:"attestation-type"` AuthenticatorGUID []byte `bson:"authenticator-guid"` AuthenticatorSignCount uint32 `bson:"authenticator-sign-count"` } const credentialCollection = "credential" // AddMFACredential stores the specified multi-factor credential. func (s *identityStore) AddMFACredential(ctx context.Context, cred store.MFACredential) error { coll := s.b.c(ctx, credentialCollection) defer coll.Database.Session.Close() err := coll.Insert(&credentialDoc{ Id: cred.ID, ProviderID: cred.ProviderID, Name: cred.Name, PublicKey: cred.PublicKey, AttestationType: cred.AttestationType, AuthenticatorGUID: cred.AuthenticatorGUID, AuthenticatorSignCount: cred.AuthenticatorSignCount, }) if err != nil { if mgo.IsDup(err) { return errgo.WithCausef(nil, store.ErrDuplicateCredential, "credential with name %q already exists", cred.Name) } return errgo.Mask(err) } return nil } // RemoveMFACredential removes the multi-factor credential with the // specified identity-id and credential name. func (s *identityStore) RemoveMFACredential(ctx context.Context, providerID, name string) error { coll := s.b.c(ctx, credentialCollection) defer coll.Database.Session.Close() err := coll.Remove(bson.M{"provider-id": store.ProviderIdentity(providerID), "name": name}) if err != nil { return errgo.Mask(err) } return nil } // ClearMFACredentials removes all multi-factor credentials for the specified user. func (s *identityStore) ClearMFACredentials(ctx context.Context, providerID string) error { coll := s.b.c(ctx, credentialCollection) defer coll.Database.Session.Close() _, err := coll.RemoveAll(bson.M{"provider-id": store.ProviderIdentity(providerID)}) if err != nil { return errgo.Mask(err) } return nil } // UserMFACredentials returns all multi-factor credentials for the specified user. func (s *identityStore) UserMFACredentials(ctx context.Context, providerID string) ([]store.MFACredential, error) { coll := s.b.c(ctx, credentialCollection) defer coll.Database.Session.Close() q := coll.Find(bson.M{"provider-id": providerID}) it := q.Iter() var credentials []store.MFACredential var doc credentialDoc for it.Next(&doc) { credentials = append(credentials, store.MFACredential{ ID: doc.Id, ProviderID: doc.ProviderID, Name: doc.Name, PublicKey: doc.PublicKey, AttestationType: doc.AttestationType, AuthenticatorGUID: doc.AuthenticatorGUID, AuthenticatorSignCount: doc.AuthenticatorSignCount, }) } if err := it.Err(); err != nil { return nil, errgo.Mask(err) } return credentials, nil } // IncrementMFACredentialSignCount increments the multi-factor credential sign count. func (s *identityStore) IncrementMFACredentialSignCount(ctx context.Context, credentialID []byte) error { coll := s.b.c(ctx, credentialCollection) defer coll.Database.Session.Close() err := coll.Update(bson.M{"_id": credentialID}, bson.M{"$inc": bson.M{"authenticator-sign-count": 1}}) if err != nil { if errgo.Cause(err) == mgo.ErrNotFound { return errgo.WithCausef(nil, store.ErrNotFound, "credential not found") } return errgo.Mask(err) } return nil } func ensureCredentialsIndexes(db *mgo.Database) error { coll := db.C(credentialCollection) indexes := []mgo.Index{{ Key: []string{"provider-id", "name"}, Unique: true, }} for _, index := range indexes { if err := coll.EnsureIndex(index); err != nil { return errgo.Mask(err) } } return nil } golang-github-canonical-candid-1.12.3/store/mgostore/doc.go000066400000000000000000000065401457263123000235710ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package mgostore import ( "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/loggo" "github.com/juju/mgo/v2/bson" "github.com/canonical/candid/store" ) var logger = loggo.GetLogger("candid.store.mgostore") // fieldNames provides the name used in the mongo documents for each // field. var fieldNames = []string{ store.ProviderID: "providerid", store.Username: "username", store.Name: "name", store.Email: "email", store.Groups: "groups", store.PublicKeys: "publickeys", store.LastLogin: "lastlogin", store.LastDischarge: "lastdischarge", store.ProviderInfo: "providerinfo", store.ExtraInfo: "extrainfo", store.Owner: "owner", } // identityDocument holds the in-database representation of a user in the identities // Mongo collection. type identityDocument struct { // ID is the internal mongodb id for the identity. ID bson.ObjectId `bson:"_id"` // ProviderID holds the identity provider specific id for the user. ProviderID string // Username holds the unique name for the user of the system, which is // associated to the URL accessed through jaas.io/u/username. Username string // Email holds the email address of the user. Email string // Name holds the display name of the user. Name string // Groups holds a list of group names to which the user belongs. Groups []string // PublicKeys contains a list of public keys associated with this account. PublicKeys_ [][]byte `bson:"publickeys"` // LastLoginTime holds the time of the last login for this identity. LastLogin time.Time // LastDischargeTime holds the time of the last discharge for this identity. LastDischarge time.Time // ProviderInfo holds additional information about the user that // is provider specific. ProviderInfo map[string][]string // ExtraInfo holds additional information about the user that is // required by other parts of the system. ExtraInfo map[string][]string // Owner holds the provider id of the owner. Owner string } // PublicKeys converts the stored public keys into the format used by the // bakery. func (d identityDocument) PublicKeys() []bakery.PublicKey { pks := make([]bakery.PublicKey, len(d.PublicKeys_)) i := 0 for _, data := range d.PublicKeys_ { // Filter out any keys that cannot be unmarshaled; there // shouldn't be any anyway. if err := pks[i].UnmarshalBinary(data); err != nil { logger.Warningf("cannot unmarshal public key: %s", err) continue } i++ } return pks[:i] } type updateDocument struct { Set bson.D `bson:"$set,omitempty"` Unset bson.D `bson:"$unset,omitempty"` AddToSet bson.D `bson:"$addToSet,omitempty"` PullAll bson.D `bson:"$pullAll,omitempty"` } func (d *updateDocument) addUpdate(op store.Operation, name string, v interface{}) { switch op { case store.NoUpdate: case store.Set: d.Set = append(d.Set, bson.DocElem{name, v}) case store.Clear: d.Unset = append(d.Unset, bson.DocElem{name, ""}) case store.Push: d.AddToSet = append(d.AddToSet, bson.DocElem{name, bson.D{{"$each", v}}}) case store.Pull: d.PullAll = append(d.PullAll, bson.DocElem{name, v}) default: panic("invalid update operation") } } func (d *updateDocument) IsZero() bool { return len(d.Set)+len(d.Unset)+len(d.AddToSet)+len(d.PullAll) == 0 } golang-github-canonical-candid-1.12.3/store/mgostore/export_test.go000066400000000000000000000005241457263123000254000ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package mgostore import ( "context" "time" "github.com/canonical/candid/meeting" ) var PutAtTime = func(ctx context.Context, s meeting.Store, id, address string, now time.Time) error { return s.(*meetingStore).put(ctx, id, address, now) } golang-github-canonical-candid-1.12.3/store/mgostore/keyvalue.go000066400000000000000000000007351457263123000246510ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package mgostore import ( "context" "github.com/juju/simplekv" "github.com/juju/simplekv/mgosimplekv" ) // an providerDataStore implements store.ProviderDataStore. type providerDataStore struct { backend *backend } func (s *providerDataStore) KeyValueStore(ctx context.Context, idp string) (simplekv.Store, error) { return mgosimplekv.NewStore(s.backend.db.C("kv" + idp)) } golang-github-canonical-candid-1.12.3/store/mgostore/meeting.go000066400000000000000000000067771457263123000244700ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package mgostore import ( "context" "strconv" "time" "github.com/juju/mgo/v2" "github.com/juju/mgo/v2/bson" "github.com/juju/utils/v2/debugstatus" "gopkg.in/errgo.v1" ) type doc struct { Id string `bson:"_id"` Addr string Created time.Time } const meetingCollection = "meeting" // meetingStore is an implementation of meeting.Store that uses a mongodb // collection for the persistent data store. type meetingStore struct { b *backend } // Context implements meeting.Store.Context. func (s *meetingStore) Context(ctx context.Context) (_ context.Context, cancel func()) { return s.b.context(ctx) } // Put implements meeting.Store.Put. func (s *meetingStore) Put(ctx context.Context, id, address string) error { return s.put(ctx, id, address, time.Now()) } // put is the internal version of Put which takes a time // for testing purposes. func (s *meetingStore) put(ctx context.Context, id, address string, now time.Time) error { coll := s.b.c(ctx, meetingCollection) defer coll.Database.Session.Close() err := coll.Insert(&doc{ Id: id, Addr: address, Created: now, }) if err != nil { return errgo.Mask(err) } return nil } // Get implements meeting.Store.Get. func (s *meetingStore) Get(ctx context.Context, id string) (address string, err error) { coll := s.b.c(ctx, meetingCollection) defer coll.Database.Session.Close() var entry doc err = coll.FindId(id).One(&entry) if err == mgo.ErrNotFound { err = errgo.Newf("rendezvous not found, probably expired") } if err != nil { return "", errgo.Mask(err) } return entry.Addr, nil } // Remove implements meeting.Store.Remove. func (s *meetingStore) Remove(ctx context.Context, id string) (time.Time, error) { coll := s.b.c(ctx, meetingCollection) defer coll.Database.Session.Close() var entry doc change := mgo.Change{ Remove: true, } _, err := coll.FindId(id).Apply(change, &entry) if err == mgo.ErrNotFound { return time.Time{}, nil } if err != nil && err != mgo.ErrNotFound { return time.Time{}, errgo.Mask(err) } return entry.Created, nil } // RemoveOld implements meeting.Store.RemoveOld. func (s *meetingStore) RemoveOld(ctx context.Context, addr string, olderThan time.Time) (ids []string, err error) { coll := s.b.c(ctx, meetingCollection) defer coll.Database.Session.Close() query := bson.D{{"created", bson.D{{"$lt", olderThan}}}} if addr != "" { query = append(query, bson.DocElem{"addr", addr}) } iter := coll.Find(query).Select(nil).Iter() var entry doc for iter.Next(&entry) { err := coll.RemoveId(entry.Id) if err != nil { return ids, errgo.Notef(err, "cannot remove %q", entry.Id) } ids = append(ids, entry.Id) } if err := iter.Err(); err != nil { return ids, errgo.Mask(err) } return ids, nil } var indexes = []mgo.Index{{ Key: []string{"addr", "created"}, }, { Key: []string{"created"}, }} func ensureMeetingIndexes(db *mgo.Database) error { coll := db.C(meetingCollection) for _, idx := range indexes { if err := coll.EnsureIndex(idx); err != nil { return errgo.Mask(err) } } return nil } func (b *backend) meetingStatus(ctx context.Context) (key string, result debugstatus.CheckResult) { result.Name = "count of meeting collection" result.Passed = true coll := b.c(ctx, meetingCollection) defer coll.Database.Session.Close() c, err := coll.Count() result.Value = strconv.Itoa(c) if err != nil { result.Value = err.Error() result.Passed = false } return "meeting_count", result } golang-github-canonical-candid-1.12.3/store/mgostore/mgostore_test.go000066400000000000000000000041301457263123000257130ustar00rootroot00000000000000package mgostore_test import ( "context" "os" "testing" "time" qt "github.com/frankban/quicktest" aclstore "github.com/juju/aclstore/v2" "github.com/juju/mgotest" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/meeting" "github.com/canonical/candid/store" "github.com/canonical/candid/store/mgostore" "github.com/canonical/candid/store/storetest" ) func TestKeyValueStore(t *testing.T) { c := qt.New(t) defer c.Done() storetest.TestKeyValueStore(c, func(c *qt.C) store.ProviderDataStore { return newFixture(c).backend.ProviderDataStore() }) } func TestStore(t *testing.T) { c := qt.New(t) defer c.Done() storetest.TestStore(c, func(c *qt.C) store.Store { return newFixture(c).backend.Store() }) } func TestMeetingStore(t *testing.T) { c := qt.New(t) defer c.Done() storetest.TestMeetingStore(c, func(c *qt.C) meeting.Store { return newFixture(c).backend.MeetingStore() }, mgostore.PutAtTime) } func TestACLStore(t *testing.T) { c := qt.New(t) defer c.Done() storetest.TestACLStore(c, func(c *qt.C) aclstore.ACLStore { return newFixture(c).backend.ACLStore() }) } func TestRootKeyStore(t *testing.T) { c := qt.New(t) defer c.Done() f := newFixture(c) rks := f.backend.BakeryRootKeyStore() ctx := context.Background() key, id, err := rks.RootKey(ctx) c.Assert(err, qt.IsNil) key2, err := rks.Get(ctx, id) c.Assert(err, qt.IsNil) c.Assert(key2, qt.DeepEquals, key) } type fixture struct { backend store.Backend db *mgotest.Database connStr string } func newFixture(c *qt.C) *fixture { db, err := mgotest.New() if errgo.Cause(err) == mgotest.ErrDisabled { c.Skip("mgotest disabled") } c.Assert(err, qt.IsNil) // mgotest sets the SocketTimout to 1s. Restore it back to the // default value. db.Session.SetSocketTimeout(time.Minute) backend, err := mgostore.NewBackend(db.Database) if err != nil { db.Close() c.Fatal(err) } c.Assert(err, qt.IsNil) c.Defer(backend.Close) connStr := os.Getenv("MGOCONNECTIONSTRING") if connStr == "" { connStr = "localhost" } return &fixture{ db: db, backend: backend, connStr: connStr, } } golang-github-canonical-candid-1.12.3/store/mgostore/store.go000066400000000000000000000216011457263123000241530ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package mgostore import ( "context" "fmt" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" mgo "github.com/juju/mgo/v2" "github.com/juju/mgo/v2/bson" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/store" ) const identitiesCollection = "identities" // identityStore is a store.Store implementation that uses a mongodb database to // store the data. type identityStore struct { b *backend } func (s *identityStore) Context(ctx context.Context) (_ context.Context, cancel func()) { return s.b.context(ctx) } // Identity implements store.Store.Identity by retrieving the specified // identity from the mongodb database. The given context must have a // mgo.Session added using ContextWithSession. func (s *identityStore) Identity(ctx context.Context, identity *store.Identity) error { coll := s.b.c(ctx, identitiesCollection) defer coll.Database.Session.Close() var doc identityDocument if err := coll.Find(identityQuery(identity)).One(&doc); err != nil { if errgo.Cause(err) == mgo.ErrNotFound { return store.NotFoundError(identity.ID, identity.ProviderID, identity.Username) } return errgo.Mask(err) } identity.ID = doc.ID.Hex() identity.ProviderID = store.ProviderIdentity(doc.ProviderID) identity.Username = doc.Username identity.Name = doc.Name identity.Email = doc.Email identity.Groups = doc.Groups identity.PublicKeys = doc.PublicKeys() identity.LastLogin = doc.LastLogin identity.LastDischarge = doc.LastDischarge identity.ProviderInfo = doc.ProviderInfo identity.ExtraInfo = doc.ExtraInfo identity.Owner = store.ProviderIdentity(doc.Owner) return nil } func identityQuery(identity *store.Identity) bson.D { switch { case identity.ID != "": if !bson.IsObjectIdHex(identity.ID) { break } return bson.D{{"_id", bson.ObjectIdHex(identity.ID)}} case identity.ProviderID != "": return bson.D{{"providerid", identity.ProviderID}} case identity.Username != "": return bson.D{{"username", identity.Username}} default: } // The identity specifies no identifying fields, return something // that will fail. return bson.D{{"_id", ""}} } // FindIdentities implements store.Store.FindIdentities by querying the // mongodb database. The given context must have a mgo.Session added // using ContextWithSession. func (s *identityStore) FindIdentities(ctx context.Context, ref *store.Identity, filter store.Filter, sort []store.Sort, skip, limit int) ([]store.Identity, error) { coll := s.b.c(ctx, identitiesCollection) defer coll.Database.Session.Close() q := coll.Find(makeQuery(ref, filter)) if len(sort) > 0 { ssort := make([]string, len(sort)) for i, s := range sort { if s.Descending { ssort[i] = fmt.Sprintf("-%s", fieldNames[s.Field]) } else { ssort[i] = fieldNames[s.Field] } } q = q.Sort(ssort...) } if skip > 0 { q = q.Skip(skip) } if limit > 0 { q = q.Limit(limit) } it := q.Iter() identities := make([]store.Identity, 0, limit) var doc identityDocument for it.Next(&doc) { identities = append(identities, store.Identity{ ID: doc.ID.Hex(), ProviderID: store.ProviderIdentity(doc.ProviderID), Username: doc.Username, Email: doc.Email, Name: doc.Name, Groups: doc.Groups, PublicKeys: doc.PublicKeys(), LastLogin: doc.LastLogin, LastDischarge: doc.LastDischarge, ProviderInfo: doc.ProviderInfo, ExtraInfo: doc.ExtraInfo, Owner: store.ProviderIdentity(doc.Owner), }) } if err := it.Err(); err != nil { return nil, errgo.Mask(err) } return identities, nil } func makeQuery(ref *store.Identity, filter store.Filter) bson.D { query := make(bson.D, 0, store.NumFields) query = appendComparison(query, fieldNames[store.ProviderID], filter[store.ProviderID], ref.ProviderID) query = appendComparison(query, fieldNames[store.Username], filter[store.Username], ref.Username) query = appendComparison(query, fieldNames[store.Name], filter[store.Name], ref.Name) query = appendComparison(query, fieldNames[store.Email], filter[store.Email], ref.Email) query = appendComparison(query, fieldNames[store.LastLogin], filter[store.LastLogin], ref.LastLogin) query = appendComparison(query, fieldNames[store.LastDischarge], filter[store.LastDischarge], ref.LastDischarge) query = appendComparison(query, fieldNames[store.Owner], filter[store.Owner], ref.Owner) return query } func appendComparison(query bson.D, fieldName string, p store.Comparison, value interface{}) bson.D { switch p { case store.NoComparison: return query case store.Equal: // TODO with Mongo 3.0, we could remove this special case // and use $eq instead. return append(query, bson.DocElem{fieldName, value}) default: return append(query, bson.DocElem{fieldName, bson.D{{comparisonOps[p], value}}}) } } var comparisonOps = []string{ store.NotEqual: "$ne", store.GreaterThan: "$gt", store.LessThan: "$lt", store.GreaterThanOrEqual: "$gte", store.LessThanOrEqual: "$lte", } // UpdateIdentity implements store.Store.UpdateIdentity by writing the // identity update to the mongodb database. The given context must have a // mgo.Session added using ContextWithSession. func (s *identityStore) UpdateIdentity(ctx context.Context, identity *store.Identity, update store.Update) error { coll := s.b.c(ctx, identitiesCollection) defer coll.Database.Session.Close() if identity.ID == "" && identity.ProviderID != "" && identity.Username != "" && update[store.Username] == store.Set { return errgo.Mask(s.upsertIdentity(coll, identity, update), errgo.Is(store.ErrDuplicateUsername)) } updateDoc := identityUpdate(identity, update) if updateDoc.IsZero() { identity := store.Identity{ ID: identity.ID, ProviderID: identity.ProviderID, Username: identity.Username, } return errgo.Mask(s.Identity(ctx, &identity), errgo.Is(store.ErrNotFound)) } err := coll.Update(identityQuery(identity), updateDoc) if err == nil { return nil } if err == mgo.ErrNotFound { return store.NotFoundError(identity.ID, identity.ProviderID, identity.Username) } if mgo.IsDup(err) { return store.DuplicateUsernameError(identity.Username) } return errgo.Mask(err) } func (s *identityStore) upsertIdentity(coll *mgo.Collection, identity *store.Identity, update store.Update) error { changeInfo, err := coll.Upsert(bson.D{{"providerid", identity.ProviderID}}, identityUpdate(identity, update)) if err != nil { if mgo.IsDup(err) { return store.DuplicateUsernameError(identity.Username) } return errgo.Mask(err) } id, ok := changeInfo.UpsertedId.(bson.ObjectId) if ok { identity.ID = id.Hex() } return nil } func identityUpdate(identity *store.Identity, update store.Update) updateDocument { var doc updateDocument doc.addUpdate(update[store.Username], fieldNames[store.Username], identity.Username) doc.addUpdate(update[store.Name], fieldNames[store.Name], identity.Name) doc.addUpdate(update[store.Email], fieldNames[store.Email], identity.Email) doc.addUpdate(update[store.Groups], fieldNames[store.Groups], identity.Groups) doc.addUpdate(update[store.PublicKeys], fieldNames[store.PublicKeys], encodePublicKeys(identity.PublicKeys)) doc.addUpdate(update[store.LastLogin], fieldNames[store.LastLogin], identity.LastLogin) doc.addUpdate(update[store.LastDischarge], fieldNames[store.LastDischarge], identity.LastDischarge) for k, v := range identity.ProviderInfo { doc.addUpdate(update[store.ProviderInfo], fieldNames[store.ProviderInfo]+"."+k, v) } for k, v := range identity.ExtraInfo { doc.addUpdate(update[store.ExtraInfo], fieldNames[store.ExtraInfo]+"."+k, v) } doc.addUpdate(update[store.Owner], fieldNames[store.Owner], identity.Owner) return doc } func encodePublicKeys(pks []bakery.PublicKey) [][]byte { data := make([][]byte, len(pks)) for i, pk := range pks { b, _ := pk.MarshalBinary() data[i] = b } return data } func ensureIdentityIndexes(db *mgo.Database) error { coll := db.C(identitiesCollection) indexes := []mgo.Index{{ Key: []string{"username"}, Unique: true, }, { Key: []string{"providerid"}, Unique: true, }} for _, index := range indexes { if err := coll.EnsureIndex(index); err != nil { return errgo.Mask(err) } } return nil } var identityCountMapReduce = mgo.MapReduce{ Map: `function() {p = this.providerid.split(':', 1); emit(p[0], 1)}`, Reduce: `function(key, values){ return Array.sum(values) }`, } // IdentityCounts implements store.Store.IdentityCounts. func (s *identityStore) IdentityCounts(ctx context.Context) (map[string]int, error) { coll := s.b.c(ctx, identitiesCollection) defer coll.Database.Session.Close() counts := make(map[string]int) var result []struct { ID string `bson:"_id"` Value int } if _, err := coll.Find(nil).MapReduce(&identityCountMapReduce, &result); err != nil { return nil, errgo.Mask(err) } for _, res := range result { counts[res.ID] = res.Value } return counts, nil } golang-github-canonical-candid-1.12.3/store/sqlstore/000077500000000000000000000000001457263123000225055ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/store/sqlstore/backend.go000066400000000000000000000146461457263123000244360ustar00rootroot00000000000000package sqlstore import ( "bytes" "database/sql" "strings" "text/template" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/postgresrootkeystore" "github.com/juju/aclstore/v2" "github.com/juju/simplekv/sqlsimplekv" "github.com/juju/utils/v2/debugstatus" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/meeting" "github.com/canonical/candid/store" ) // backend provides a wrapper around an SQL database that can be used // as the persistent storage for the various types of store required by // the identity service. type backend struct { db *sql.DB driver *driver rootKeys *postgresrootkeystore.RootKeys aclStore aclstore.ACLStore } // NewBackend creates a new store.Backend implementation using the // given driverName and *sql.DB. The driverName must match the value // used to open the database. // // Closing the returned Backend will also close db. func NewBackend(driverName string, db *sql.DB) (store.Backend, error) { if driverName != "postgres" { return nil, errgo.Newf("unsupported database driver %q", driverName) } driver, err := newPostgresDriver(db) if err != nil { return nil, errgo.Notef(err, "cannot initialise database") } rootkeys := postgresrootkeystore.NewRootKeys(db, "rootkeys", 1000) defer rootkeys.Close() aclStore, err := sqlsimplekv.NewStore(driverName, db, "acls") if err != nil { return nil, errgo.Mask(err) } return &backend{ db: db, driver: driver, rootKeys: postgresrootkeystore.NewRootKeys(db, "rootkeys", 1000), aclStore: aclstore.NewACLStore(aclStore), }, nil } func (b *backend) Close() { b.rootKeys.Close() b.db.Close() } // Store returns a new store.Store implementation using this database for // persistent storage. func (b *backend) Store() store.Store { return &identityStore{b} } func (b *backend) BakeryRootKeyStore() bakery.RootKeyStore { return b.rootKeys.NewStore(postgresrootkeystore.Policy{ ExpiryDuration: 365 * 24 * time.Hour, }) } // ProviderDataStore returns a new store.ProviderDataStore implementation // using this database for persistent storage. func (b *backend) ProviderDataStore() store.ProviderDataStore { return &providerDataStore{b} } // MeetingStore returns a new meeting.Stor implementation using this // database for persistent storage. func (b *backend) MeetingStore() meeting.Store { return &meetingStore{b} } func (b *backend) ACLStore() aclstore.ACLStore { return b.aclStore } // DebugStatusCheckerFuncs implements store.Backend.DebugStatusCheckerFuncs. func (b *backend) DebugStatusCheckerFuncs() []debugstatus.CheckerFunc { return nil } // withTx runs f in a new transaction. any error returned by f will not // have it's cause masked. func (b *backend) withTx(f func(*sql.Tx) error) error { tx, err := b.db.Begin() if err != nil { return errgo.Mask(err) } if err := f(tx); err != nil { if err := tx.Rollback(); err != nil { logger.Errorf("failed to rollback transaction: %s", err) } return errgo.Mask(err, errgo.Any) } return errgo.Mask(tx.Commit()) } type tmplID int const ( tmplClearIdentitySet tmplID = iota tmplClearMFACredentials tmplFindIdentities tmplFindMeetings tmplGetMeeting tmplGetMFACredentials tmplGetProviderData tmplGetProviderDataForUpdate tmplIdentityCounts tmplIdentityFrom tmplIdentityID tmplIncrementMFACredentialSignCount tmplInsertMFACredential tmplInsertProviderData tmplPullIdentitySet tmplPushIdentitySet tmplPutMeeting tmplRemoveMeetings tmplRemoveMFACredential tmplSelectIdentitySet tmplUpdateIdentity tmplUpsertIdentity numTmpl ) type queryer interface { Exec(query string, args ...interface{}) (sql.Result, error) Query(query string, args ...interface{}) (*sql.Rows, error) QueryRow(query string, args ...interface{}) *sql.Row } // argBuilder is an interface that can be embedded in template parameters // to record the arguments needed to be supplied with SQL queries. type argBuilder interface { // Arg is a method that is called in templates with the value of // the next argument to be used in the query. Arg should remember // the value and return a valid placeholder to access that // argument when executing the query. Arg(interface{}) string // args returns the slice of arguments that should be used when // executing the query. args() []interface{} } type driver struct { name string tmpls [numTmpl]*template.Template argBuilderFunc func() argBuilder isDuplicateFunc func(error) bool } // exec performs the Exec method on the given queryer by processing the // given template with the given params to determine the query to // execute. func (d *driver) exec(q queryer, tmplID tmplID, params argBuilder) (sql.Result, error) { query, err := d.executeTemplate(tmplID, params) if err != nil { return nil, errgo.Notef(err, "cannot build query") } res, err := q.Exec(query, params.args()...) return res, errgo.Mask(err, errgo.Any) } // query performs the Query method on the given queryer by processing the // given template with the given params to determine the query to // execute. func (d *driver) query(q queryer, tmplID tmplID, params argBuilder) (*sql.Rows, error) { query, err := d.executeTemplate(tmplID, params) if err != nil { return nil, errgo.Notef(err, "cannot build query") } rows, err := q.Query(query, params.args()...) return rows, errgo.Mask(err, errgo.Any) } // queryRow performs the QueryRow method on the given queryer by // processing the given template with the given params to determine the // query to execute. func (d *driver) queryRow(q queryer, tmplID tmplID, params argBuilder) (*sql.Row, error) { query, err := d.executeTemplate(tmplID, params) if err != nil { return nil, errgo.Notef(err, "cannot build query") } return q.QueryRow(query, params.args()...), nil } func (d *driver) parseTemplate(tmplID tmplID, tmpl string) error { var err error d.tmpls[tmplID], err = template.New("").Funcs(template.FuncMap{ "join": strings.Join, }).Parse(tmpl) return errgo.Mask(err) } func (d *driver) executeTemplate(tmplID tmplID, params interface{}) (string, error) { buf := new(bytes.Buffer) if err := d.tmpls[tmplID].Execute(buf, params); err != nil { return "", errgo.Mask(err) } return buf.String(), nil } var comparisons = map[store.Comparison]string{ store.Equal: "=", store.NotEqual: "<>", store.GreaterThan: ">", store.LessThan: "<", store.GreaterThanOrEqual: ">=", store.LessThanOrEqual: "<=", } golang-github-canonical-candid-1.12.3/store/sqlstore/config.go000066400000000000000000000016661457263123000243120ustar00rootroot00000000000000package sqlstore import ( "database/sql" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/store" ) // Params holds the specification for the parameters // used in the config file. type Params struct { ConnectionString string `yaml:"connection-string"` } func init() { store.Register("postgres", unmarshalBackend) } func unmarshalBackend(unmarshal func(interface{}) error) (store.BackendFactory, error) { var p Params if err := unmarshal(&p); err != nil { return nil, errgo.Mask(err) } return p, nil } // NewBackend implements store.BackendFactory. func (p Params) NewBackend() (store.Backend, error) { logger.Infof("connecting to postgresql") db, err := sql.Open("postgres", p.ConnectionString) if err != nil { return nil, errgo.Notef(err, "cannot connect to database") } backend, err := NewBackend("postgres", db) if err != nil { return nil, errgo.Notef(err, "cannot initialise database") } return backend, nil } golang-github-canonical-candid-1.12.3/store/sqlstore/config_test.go000066400000000000000000000005271457263123000253440ustar00rootroot00000000000000package sqlstore_test import ( "testing" qt "github.com/frankban/quicktest" "github.com/canonical/candid/store/storetest" ) func TestConfigUnmarshal(t *testing.T) { c := qt.New(t) defer c.Done() f := newFixture(c) storetest.TestUnmarshal(c, ` storage: type: postgres connection-string: 'search_path=`+f.pg.Schema()+`' `) } golang-github-canonical-candid-1.12.3/store/sqlstore/credential.go000066400000000000000000000070171457263123000251530ustar00rootroot00000000000000// Copyright 2021 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package sqlstore import ( "context" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/store" ) type userCredentialParams struct { argBuilder ProviderID store.ProviderIdentity Name string ID []byte PublicKey []byte AttestationType string AuthenticatorGUID []byte AuthenticatorSignCount uint32 } // AddMFACredential stores the specified multi-factor credential. func (s *identityStore) AddMFACredential(ctx context.Context, cred store.MFACredential) error { params := &userCredentialParams{ argBuilder: s.driver.argBuilderFunc(), ProviderID: cred.ProviderID, Name: cred.Name, ID: cred.ID, PublicKey: cred.PublicKey, AttestationType: cred.AttestationType, AuthenticatorGUID: cred.AuthenticatorGUID, AuthenticatorSignCount: cred.AuthenticatorSignCount, } _, err := s.driver.exec(s.db, tmplInsertMFACredential, params) if err != nil { if postgresIsDuplicate(errgo.Cause(err)) { return errgo.WithCausef(nil, store.ErrDuplicateCredential, "credential with name %q already exists", cred.Name) } return errgo.Mask(err) } return nil } // RemoveMFACredential removes the multi-factor credential with the // specified username and credential name. func (s *identityStore) RemoveMFACredential(ctx context.Context, providerID, name string) error { params := &userCredentialParams{ argBuilder: s.driver.argBuilderFunc(), ProviderID: store.ProviderIdentity(providerID), Name: name, } _, err := s.driver.exec(s.db, tmplRemoveMFACredential, params) if err != nil { return errgo.Mask(err) } return nil } // ClearMFACredentials removes all multi-factor credentials for the specified user. func (s *identityStore) ClearMFACredentials(ctx context.Context, providerID string) error { params := &userCredentialParams{ argBuilder: s.driver.argBuilderFunc(), ProviderID: store.ProviderIdentity(providerID), } _, err := s.driver.exec(s.db, tmplClearMFACredentials, params) if err != nil { return errgo.Mask(err) } return nil } // UserMFACredentials returns all multi-factor credentials for the specified user. func (s *identityStore) UserMFACredentials(ctx context.Context, providerID string) ([]store.MFACredential, error) { params := &userCredentialParams{ argBuilder: s.driver.argBuilderFunc(), ProviderID: store.ProviderIdentity(providerID), } rows, err := s.driver.query(s.db, tmplGetMFACredentials, params) if err != nil { return nil, errgo.Mask(err) } defer rows.Close() var credentials []store.MFACredential for rows.Next() { var cred store.MFACredential if err := rows.Scan(&cred.ID, &cred.ProviderID, &cred.Name, &cred.PublicKey, &cred.AttestationType, &cred.AuthenticatorGUID, &cred.AuthenticatorSignCount, ); err != nil { return nil, errgo.Mask(err) } credentials = append(credentials, cred) } if err := rows.Err(); err != nil { return nil, errgo.Mask(err) } return credentials, nil } // IncrementMFACredentialSignCount increments the multi-factor credential sign count. func (s *identityStore) IncrementMFACredentialSignCount(ctx context.Context, credentialID []byte) error { params := &userCredentialParams{ argBuilder: s.driver.argBuilderFunc(), ID: credentialID, } _, err := s.driver.exec(s.db, tmplIncrementMFACredentialSignCount, params) if err != nil { return errgo.Mask(err) } return nil } golang-github-canonical-candid-1.12.3/store/sqlstore/export_test.go000066400000000000000000000005171457263123000254170ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package sqlstore import ( "context" "time" "github.com/canonical/candid/meeting" ) var PutAtTime = func(ctx context.Context, s meeting.Store, id, address string, now time.Time) error { return s.(*meetingStore).put(id, address, now) } golang-github-canonical-candid-1.12.3/store/sqlstore/keyvalue.go000066400000000000000000000007371457263123000246700ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package sqlstore import ( "context" "github.com/juju/simplekv" "github.com/juju/simplekv/sqlsimplekv" ) // A providerDataStore implements store.ProviderDataStore. type providerDataStore struct { b *backend } func (s *providerDataStore) KeyValueStore(_ context.Context, idp string) (simplekv.Store, error) { return sqlsimplekv.NewStore(s.b.driver.name, s.b.db, "idpkv_"+idp) } golang-github-canonical-candid-1.12.3/store/sqlstore/meeting.go000066400000000000000000000066421457263123000244740ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package sqlstore import ( "context" "database/sql" "time" "gopkg.in/errgo.v1" ) // meetingStore is an implementation of meeting.Store that uses an sql // table. type meetingStore struct { *backend } // Context implements meeting.Store.Context. func (s *meetingStore) Context(ctx context.Context) (_ context.Context, cancel func()) { return ctx, func() {} } // Put implements meeting.Store.Put. func (s *meetingStore) Put(_ context.Context, id, address string) error { return s.put(id, address, time.Now()) } type meetingParams struct { argBuilder ID string Address string Time time.Time } // put is the internal version of Put which takes a time // for testing purposes. func (s *meetingStore) put(id, address string, now time.Time) error { params := &meetingParams{ argBuilder: s.driver.argBuilderFunc(), ID: id, Address: address, Time: now, } _, err := s.driver.exec(s.db, tmplPutMeeting, params) return errgo.Mask(err) } // Get implements meeting.Store.Get. func (s *meetingStore) Get(_ context.Context, id string) (string, error) { params := &meetingParams{ argBuilder: s.driver.argBuilderFunc(), ID: id, } var address string var created time.Time row, err := s.driver.queryRow(s.db, tmplGetMeeting, params) if err != nil { return "", errgo.Mask(err) } err = row.Scan(&address, &created) if errgo.Cause(err) == sql.ErrNoRows { return "", errgo.Newf("rendezvous not found, probably expired") } return address, errgo.Mask(err) } type removeMeetingParams struct { argBuilder IDs []string } // Remove implements meeting.Store.Remove. func (s *meetingStore) Remove(_ context.Context, id string) (time.Time, error) { var created time.Time err := s.withTx(func(tx *sql.Tx) error { params := &meetingParams{ argBuilder: s.driver.argBuilderFunc(), ID: id, } row, err := s.driver.queryRow(tx, tmplGetMeeting, params) if err != nil { return errgo.Mask(err) } var address string err = row.Scan(&address, &created) if err != nil { return errgo.Mask(err, errgo.Is(sql.ErrNoRows)) } removeParams := removeMeetingParams{ argBuilder: s.driver.argBuilderFunc(), IDs: []string{id}, } params.argBuilder = s.driver.argBuilderFunc() _, err = s.driver.exec(tx, tmplRemoveMeetings, removeParams) return errgo.Mask(err) }) if errgo.Cause(err) == sql.ErrNoRows { return time.Time{}, nil } return created, errgo.Mask(err) } // RemoveOld implements meeting.Store.RemoveOld. func (s *meetingStore) RemoveOld(_ context.Context, addr string, olderThan time.Time) (ids []string, err error) { err = s.withTx(func(tx *sql.Tx) error { params := &meetingParams{ argBuilder: s.driver.argBuilderFunc(), Address: addr, Time: olderThan, } rows, err := s.driver.query(tx, tmplFindMeetings, params) if err != nil { return errgo.Mask(err) } for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return errgo.Mask(err) } ids = append(ids, id) } if err := rows.Err(); err != nil { return errgo.Mask(err) } if len(ids) == 0 { return nil } removeParams := removeMeetingParams{ argBuilder: s.driver.argBuilderFunc(), IDs: ids, } _, err = s.driver.exec(tx, tmplRemoveMeetings, removeParams) return errgo.Mask(err) }) if err != nil { return nil, errgo.Mask(err) } return ids, nil } golang-github-canonical-candid-1.12.3/store/sqlstore/postgres.go000066400000000000000000000170611457263123000247070ustar00rootroot00000000000000package sqlstore import ( "database/sql" "fmt" "github.com/lib/pq" errgo "gopkg.in/errgo.v1" ) const postgresInit = ` CREATE TABLE IF NOT EXISTS identities ( id SERIAL PRIMARY KEY, providerid TEXT UNIQUE NOT NULL, username TEXT UNIQUE NOT NULL, name TEXT, email TEXT, lastlogin TIMESTAMP WITH TIME ZONE, lastdischarge TIMESTAMP WITH TIME ZONE ); -- Postgresql versions before 9.6 did not support "ALTER TABLE ... ADD -- COLUMN IF NOT EXISTS...". This performs the equivalent function. DO $$ BEGIN BEGIN ALTER TABLE identities ADD COLUMN owner TEXT; EXCEPTION WHEN duplicate_column THEN RETURN; END; END; $$; CREATE TABLE IF NOT EXISTS identity_groups ( identity INTEGER REFERENCES identities NOT NULL, value TEXT NOT NULL, UNIQUE (identity, value) ); CREATE TABLE IF NOT EXISTS identity_publickeys ( identity INTEGER REFERENCES identities NOT NULL, value BYTEA NOT NULL, UNIQUE (identity, value) ); CREATE TABLE IF NOT EXISTS identity_providerinfo ( identity INTEGER REFERENCES identities NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, UNIQUE (identity, key, value) ); CREATE TABLE IF NOT EXISTS identity_extrainfo ( identity INTEGER REFERENCES identities NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, UNIQUE (identity, key, value) ); CREATE TABLE IF NOT EXISTS provider_data ( provider TEXT NOT NULL, key TEXT NOT NULL, value BYTEA NOT NULL, expire TIMESTAMP WITH TIME ZONE, UNIQUE (provider, key) ); CREATE OR REPLACE FUNCTION provider_data_expire_fn() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN DELETE FROM provider_data WHERE expire < NOW(); RETURN NEW; END; $$; CREATE INDEX IF NOT EXISTS provider_data_expire ON provider_data (expire); DROP TRIGGER IF EXISTS provider_data_expire_tr ON provider_data; CREATE TRIGGER provider_data_expire_tr BEFORE INSERT ON provider_data EXECUTE PROCEDURE provider_data_expire_fn(); CREATE TABLE IF NOT EXISTS meetings ( id TEXT NOT NULL PRIMARY KEY, address TEXT NOT NULL, created TIMESTAMP WITH TIME ZONE NOT NULL ); CREATE TABLE IF NOT EXISTS credentials ( id BYTEA PRIMARY KEY, providerid TEXT NOT NULL REFERENCES identities(providerid), name TEXT NOT NULL, public_key BYTEA NOT NULL, attestation_type TEXT NOT NULL, authenticator_guid BYTEA NOT NULL, authenticator_sign_count INTEGER, UNIQUE (providerid, name) ); ` var postgresTmpls = [numTmpl]string{ tmplIdentityFrom: ` SELECT id, providerid, username, name, email, lastlogin, lastdischarge, owner FROM identities WHERE {{.Column}}={{.Identity | .Arg}}`, tmplSelectIdentitySet: ` SELECT {{if .Key}}key, {{end}}value FROM {{.Table}} WHERE identity={{.Identity | .Arg}}`, tmplFindIdentities: ` SELECT id, providerid, username, name, email, lastlogin, lastdischarge, owner FROM identities {{if .Where}}WHERE{{range $i, $w := .Where}}{{if gt $i 0}} AND{{end}} {{$w.Column}}{{$w.Comparison}}{{$w.Value | $.Arg}}{{end}}{{end}} {{if .Sort}}ORDER BY {{join .Sort ", "}}{{end}} {{if gt .Limit 0}}LIMIT {{.Limit}}{{end}} {{if gt .Skip 0}}OFFSET {{.Skip}}{{end}}`, tmplUpdateIdentity: ` UPDATE identities SET {{range $i, $u := .Updates}}{{if gt $i 0}}, {{end}} {{$u.Column}}={{$u.Value | $.Arg}}{{end}} WHERE {{.Column}}={{.Identity | .Arg}} RETURNING id`, tmplIdentityID: ` SELECT id FROM identities WHERE {{.Column}}={{.Identity | .Arg}}`, tmplUpsertIdentity: ` INSERT INTO identities (providerid{{range .Updates}}, {{.Column}}{{end}}) VALUES ({{.Identity | .Arg}}{{range .Updates}}, {{.Value | $.Arg}}{{end}}) ON CONFLICT (providerid) DO UPDATE SET{{range $i, $u := .Updates}}{{if gt $i 0}}, {{end}} {{$u.Column}}={{$u.Value | $.Arg}}{{end}} WHERE identities.providerid={{.Identity | .Arg}} RETURNING id`, tmplClearIdentitySet: ` DELETE FROM {{.Table}} WHERE identity={{.ID | .Arg}}{{if .Key}} AND key={{.Key | .Arg}}{{end}}`, tmplPushIdentitySet: ` INSERT INTO {{.Table}} (identity, {{if .Key}}key, {{end}}value) VALUES {{range $i, $v := .Values}}{{if gt $i 0}}, {{end}}({{$.ID | $.Arg}}, {{if $.Key}}{{$.Key | $.Arg}}, {{end}}{{$v | $.Arg}}){{end}} ON CONFLICT (identity, {{if .Key}}key, {{end}}value) DO NOTHING`, tmplPullIdentitySet: ` DELETE FROM {{.Table}} WHERE identity={{.ID | $.Arg}}{{if .Key}} AND key={{.Key | $.Arg}}{{end}} AND value IN ({{range $i, $v := .Values}}{{if gt $i 0}}, {{end}}{{$v | $.Arg}}{{end}})`, tmplGetProviderData: ` SELECT value FROM provider_data WHERE provider={{.Provider | .Arg}} AND key={{.Key | .Arg}} AND (expire IS NULL OR expire > now())`, tmplGetProviderDataForUpdate: ` SELECT value FROM provider_data WHERE provider={{.Provider | .Arg}} AND key={{.Key | .Arg}} AND (expire IS NULL OR expire > now()) FOR UPDATE`, tmplInsertProviderData: ` INSERT INTO provider_data (provider, key, value, expire) VALUES ({{.Provider | .Arg}}, {{.Key | .Arg}}, {{.Value | .Arg}}, {{.Expire | .Arg}}) {{if .Update}}ON CONFLICT (provider, key) DO UPDATE SET value={{.Value | .Arg}}, expire={{.Expire | .Arg}}{{end}}`, tmplGetMeeting: ` SELECT address, created FROM meetings WHERE id={{.ID | .Arg}}`, tmplPutMeeting: ` INSERT INTO meetings (id, address, created) VALUES ({{.ID | .Arg}}, {{.Address | .Arg}}, {{.Time | .Arg}})`, tmplFindMeetings: ` SELECT id FROM meetings WHERE created < {{.Time | .Arg}}{{if .Address}} AND address={{.Address | .Arg}}{{end}}`, tmplRemoveMeetings: ` DELETE FROM meetings WHERE id IN({{range $i, $id := .IDs}}{{if gt $i 0}}, {{end}}{{$id | $.Arg}}{{end}})`, tmplIdentityCounts: ` SELECT substring(providerid, '^[^:]*') as idp, COUNT(1) FROM identities GROUP BY idp`, tmplInsertMFACredential: ` INSERT INTO credentials (id, providerid, name, public_key, attestation_type, authenticator_guid, authenticator_sign_count) VALUES ({{.ID | .Arg}}, {{.ProviderID | .Arg}}, {{.Name | .Arg}}, {{.PublicKey | .Arg}}, {{.AttestationType | .Arg}}, {{.AuthenticatorGUID | .Arg}}, {{.AuthenticatorSignCount | .Arg}})`, tmplRemoveMFACredential: ` DELETE FROM credentials WHERE providerid={{.ProviderID | .Arg}} AND name={{.Name | .Arg}}`, tmplClearMFACredentials: ` DELETE FROM credentials WHERE providerid={{.ProviderID | .Arg}}`, tmplGetMFACredentials: ` SELECT id, providerid, name, public_key, attestation_type, authenticator_guid, authenticator_sign_count FROM credentials WHERE providerid={{.ProviderID | .Arg}}`, tmplIncrementMFACredentialSignCount: ` UPDATE credentials SET authenticator_sign_count = authenticator_sign_count + 1 WHERE id={{.ID | .Arg}} AND id={{.ID | .Arg}}`, } // newPostgresDriver creates a postgres driver using the given DB. func newPostgresDriver(db *sql.DB) (*driver, error) { _, err := db.Exec(postgresInit) if err != nil { return nil, errgo.Mask(err) } d := &driver{ name: "postgres", argBuilderFunc: func() argBuilder { return &postgresArgBuilder{} }, isDuplicateFunc: postgresIsDuplicate, } for i, t := range postgresTmpls { if err := d.parseTemplate(tmplID(i), t); err != nil { return nil, errgo.Notef(err, "cannot parse template %v", t) } } return d, nil } func postgresIsDuplicate(err error) bool { if pqerr, ok := err.(*pq.Error); ok && pqerr.Code.Name() == "unique_violation" { return true } return false } // postgresArgBuilder implements an argBuilder that produces placeholders // in the the "$n" format. type postgresArgBuilder struct { args_ []interface{} } // Arg implements argbuilder.Arg. func (b *postgresArgBuilder) Arg(a interface{}) string { b.args_ = append(b.args_, a) return fmt.Sprintf("$%d", len(b.args_)) } // args implements argbuilder.args. func (b *postgresArgBuilder) args() []interface{} { return b.args_ } golang-github-canonical-candid-1.12.3/store/sqlstore/postgres_test.go000066400000000000000000000104101457263123000257350ustar00rootroot00000000000000package sqlstore_test import ( "context" "testing" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" aclstore "github.com/juju/aclstore/v2" "github.com/juju/postgrestest" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/meeting" "github.com/canonical/candid/store" "github.com/canonical/candid/store/sqlstore" "github.com/canonical/candid/store/storetest" ) func TestKeyValueStore(t *testing.T) { c := qt.New(t) defer c.Done() storetest.TestKeyValueStore(c, func(c *qt.C) store.ProviderDataStore { return newFixture(c).backend.ProviderDataStore() }) } func TestStore(t *testing.T) { c := qt.New(t) defer c.Done() storetest.TestStore(c, func(c *qt.C) store.Store { return newFixture(c).backend.Store() }) } func TestMeetingStore(t *testing.T) { c := qt.New(t) defer c.Done() storetest.TestMeetingStore(c, func(c *qt.C) meeting.Store { return newFixture(c).backend.MeetingStore() }, sqlstore.PutAtTime) } func TestACLStore(t *testing.T) { c := qt.New(t) defer c.Done() storetest.TestACLStore(c, func(c *qt.C) aclstore.ACLStore { return newFixture(c).backend.ACLStore() }) } func TestUpdateIDNotFound(t *testing.T) { c := qt.New(t) defer c.Done() f := newFixture(c) err := f.backend.Store().UpdateIdentity( context.Background(), &store.Identity{ ID: "1000000", Name: "test-user", }, store.Update{ store.Name: store.Set, }, ) c.Assert(err, qt.ErrorMatches, `identity "1000000" not found`) c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) } func TestUpdateIDEmptyNotFound(t *testing.T) { c := qt.New(t) defer c.Done() f := newFixture(c) err := f.backend.Store().UpdateIdentity( context.Background(), &store.Identity{ ID: "1000000", }, store.Update{}, ) c.Assert(err, qt.ErrorMatches, `identity "1000000" not found`) c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) } func TestUpdateUsernameEmptyNotFound(t *testing.T) { c := qt.New(t) defer c.Done() f := newFixture(c) err := f.backend.Store().UpdateIdentity( context.Background(), &store.Identity{ Username: "no-user", }, store.Update{}, ) c.Assert(err, qt.ErrorMatches, `user no-user not found`) c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) } func TestUpdateProviderIDEmptyNotFound(t *testing.T) { c := qt.New(t) defer c.Done() f := newFixture(c) err := f.backend.Store().UpdateIdentity( context.Background(), &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "no-user"), }, store.Update{}, ) c.Assert(err, qt.ErrorMatches, `identity "test:no-user" not found`) c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) } func TestInitIdempotent(t *testing.T) { c := qt.New(t) defer c.Done() f := newFixture(c) testStore := f.backend.Store() var pk1 bakery.PublicKey id1 := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "test-1"), Username: "test-1", Name: "Test User", Email: "test-1@example.com", PublicKeys: []bakery.PublicKey{pk1}, ProviderInfo: map[string][]string{ "pk1": {"pk1v1", "pk1v2"}, }, ExtraInfo: map[string][]string{ "ek1": {"ek1v1", "ek1v2"}, }, Owner: store.MakeProviderIdentity("test", "test-0"), } err := testStore.UpdateIdentity( context.Background(), &id1, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, store.PublicKeys: store.Set, store.ProviderInfo: store.Set, store.ExtraInfo: store.Set, store.Owner: store.Set, }, ) c.Assert(err, qt.IsNil) backend, err := sqlstore.NewBackend("postgres", f.pg.DB) c.Assert(err, qt.IsNil) id2 := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "test-1"), } err = backend.Store().Identity(context.Background(), &id2) c.Assert(err, qt.IsNil) c.Assert(id2, qt.DeepEquals, id1) } type fixture struct { backend store.Backend pg *postgrestest.DB } func newFixture(c *qt.C) *fixture { pg, err := postgrestest.New() if errgo.Cause(err) == postgrestest.ErrDisabled { c.Skip(err.Error()) } c.Assert(err, qt.IsNil) backend, err := sqlstore.NewBackend("postgres", pg.DB) c.Assert(err, qt.IsNil) // Note: closing backend also closes the db. c.Defer(backend.Close) return &fixture{ pg: pg, backend: backend, } } golang-github-canonical-candid-1.12.3/store/sqlstore/store.go000066400000000000000000000346301457263123000241760ustar00rootroot00000000000000package sqlstore import ( "context" "database/sql" sqldriver "database/sql/driver" "strconv" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/loggo" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/store" ) var logger = loggo.GetLogger("candid.sqlstore") var identityColumns = [store.NumFields]string{ store.ProviderID: "providerid", store.Username: "username", store.Name: "name", store.Email: "email", store.LastLogin: "lastlogin", store.LastDischarge: "lastdischarge", store.Owner: "owner", } type identityStore struct { *backend } // Context implements store.Context, it returns the given context unmodified. func (*identityStore) Context(ctx context.Context) (context.Context, func()) { return ctx, func() {} } // Identity implements store.Identity. func (s *identityStore) Identity(ctx context.Context, identity *store.Identity) error { return errgo.Mask(s.withTx(func(tx *sql.Tx) error { return s.identity(tx, identity) }), errgo.Is(store.ErrNotFound)) } type identityFromParams struct { argBuilder Column string Identity interface{} } func (s *identityStore) identity(tx *sql.Tx, identity *store.Identity) error { params := &identityFromParams{ argBuilder: s.driver.argBuilderFunc(), } switch { case identity.ID != "": params.Column = "id" params.Identity = identity.ID case identity.ProviderID != "": params.Column = "providerid" params.Identity = identity.ProviderID case identity.Username != "": params.Column = "username" params.Identity = identity.Username default: return store.NotFoundError("", "", "") } row, err := s.driver.queryRow(tx, tmplIdentityFrom, params) if err != nil { return errgo.Mask(err) } err = scanIdentity(row, identity) if errgo.Cause(err) == sql.ErrNoRows { return store.NotFoundError(identity.ID, identity.ProviderID, identity.Username) } if err != nil { return errgo.Notef(err, "cannot get identity") } if err := s.completeIdentity(tx, identity); err != nil { return errgo.Notef(err, "cannot get identity") } return nil } // FindIdentities implements store.FindIdentities. func (s *identityStore) FindIdentities(ctx context.Context, ref *store.Identity, filter store.Filter, sort []store.Sort, skip, limit int) ([]store.Identity, error) { var identities []store.Identity err := s.withTx(func(tx *sql.Tx) error { var err error identities, err = s.findIdentities(tx, ref, filter, sort, skip, limit) return err }) if err != nil { return nil, errgo.Notef(err, "cannot find identities") } return identities, nil } type where struct { Column string Comparison string Value interface{} } type findIdentitiesParams struct { argBuilder Where []where Sort []string Limit int Skip int } func (s *identityStore) findIdentities(tx *sql.Tx, ref *store.Identity, filter store.Filter, sort []store.Sort, skip, limit int) ([]store.Identity, error) { var wheres []where for f, op := range filter { col := identityColumns[f] cond := comparisons[op] if col == "" || cond == "" { continue } wheres = append(wheres, where{col, cond, fieldValue(store.Field(f), ref)}) } sorts := make([]string, 0, len(sort)) for _, s := range sort { col := identityColumns[s.Field] if col == "" { continue } if s.Descending { col += " DESC" } sorts = append(sorts, col) } params := &findIdentitiesParams{ argBuilder: s.driver.argBuilderFunc(), Where: wheres, Sort: sorts, Limit: limit, Skip: skip, } rows, err := s.driver.query(tx, tmplFindIdentities, params) if err != nil { return nil, errgo.Mask(err) } defer rows.Close() var identities []store.Identity for rows.Next() { var identity store.Identity if err := scanIdentity(rows, &identity); err != nil { return nil, errgo.Mask(err) } identities = append(identities, identity) } if err := rows.Err(); err != nil { return nil, errgo.Mask(err) } for i := range identities { if err := s.completeIdentity(tx, &identities[i]); err != nil { return nil, errgo.Mask(err) } } return identities, nil } func fieldValue(f store.Field, id *store.Identity) interface{} { switch f { case store.ProviderID: return id.ProviderID case store.Username: return id.Username case store.Name: return sql.NullString{id.Name, id.Name != ""} case store.Email: return sql.NullString{id.Email, id.Email != ""} case store.LastLogin: return nullTime{id.LastLogin, !id.LastLogin.IsZero()} case store.LastDischarge: return nullTime{id.LastDischarge, !id.LastDischarge.IsZero()} case store.Owner: return sql.NullString{string(id.Owner), id.Owner != ""} } return nil } func (s *identityStore) completeIdentity(tx *sql.Tx, identity *store.Identity) error { var err error identity.Groups, err = s.getGroups(tx, identity.ID) if err != nil { return errgo.Mask(err) } identity.PublicKeys, err = s.getPublicKeys(tx, identity.ID) if err != nil { return errgo.Mask(err) } identity.ProviderInfo, err = s.getInfoMap(tx, "identity_providerinfo", identity.ID) if err != nil { return errgo.Mask(err) } identity.ExtraInfo, err = s.getInfoMap(tx, "identity_extrainfo", identity.ID) if err != nil { return errgo.Mask(err) } return nil } type selectIdentitySetParams struct { argBuilder Table string Identity string Key bool } func (s *identityStore) getGroups(tx *sql.Tx, id string) ([]string, error) { params := selectIdentitySetParams{ argBuilder: s.driver.argBuilderFunc(), Table: "identity_groups", Identity: id, } rows, err := s.driver.query(tx, tmplSelectIdentitySet, params) if err != nil { return nil, errgo.Mask(err) } defer rows.Close() var groups []string for rows.Next() { var g string if err := rows.Scan(&g); err != nil { return nil, errgo.Mask(err) } groups = append(groups, g) } return groups, errgo.Mask(rows.Err()) } func (s *identityStore) getPublicKeys(tx *sql.Tx, id string) ([]bakery.PublicKey, error) { params := selectIdentitySetParams{ argBuilder: s.driver.argBuilderFunc(), Table: "identity_publickeys", Identity: id, } rows, err := s.driver.query(tx, tmplSelectIdentitySet, params) if err != nil { return nil, errgo.Mask(err) } defer rows.Close() var pks []bakery.PublicKey for rows.Next() { var b sql.RawBytes if err := rows.Scan(&b); err != nil { return nil, errgo.Mask(err) } var pk bakery.PublicKey if err := pk.UnmarshalBinary(b); err != nil { logger.Errorf("invalid public key in database: %s", err) continue } pks = append(pks, pk) } return pks, errgo.Mask(rows.Err()) } func (s *identityStore) getInfoMap(tx *sql.Tx, table string, id string) (map[string][]string, error) { params := selectIdentitySetParams{ argBuilder: s.driver.argBuilderFunc(), Table: table, Identity: id, Key: true, } rows, err := s.driver.query(tx, tmplSelectIdentitySet, params) if err != nil { return nil, errgo.Mask(err) } defer rows.Close() info := make(map[string][]string) for rows.Next() { var k, v string if err := rows.Scan(&k, &v); err != nil { return nil, errgo.Mask(err) } info[k] = append(info[k], v) } return info, errgo.Mask(rows.Err()) } // UpdateIdentity implements store.Store.UpdateIdentity. func (s *identityStore) UpdateIdentity(_ context.Context, identity *store.Identity, update store.Update) (err error) { return errgo.Mask(s.withTx(func(tx *sql.Tx) error { return s.updateIdentity(tx, identity, update) }), errgo.Is(store.ErrDuplicateUsername), errgo.Is(store.ErrNotFound)) } type update struct { // Column contains the column to set. Column string // Value contains the value to set the column to. Value interface{} } type updateIdentityParams struct { argBuilder // Column contains the name of the column to use to determine the // identity to be updated or returned. Column string // Identity contains the value to match with the identity column // above. Identity string // Updates contains the updates to apply. Updates []update } func (s *identityStore) updateIdentity(tx *sql.Tx, identity *store.Identity, upd store.Update) error { tmpl := tmplUpdateIdentity params := updateIdentityParams{ argBuilder: s.driver.argBuilderFunc(), } switch { case identity.ID != "": if _, err := strconv.Atoi(identity.ID); err != nil { // By definition if id isn't numeric it won't exist. return store.NotFoundError(identity.ID, "", "") } params.Column = "id" params.Identity = identity.ID case identity.ProviderID != "": if upd[store.Username] == store.Set { tmpl = tmplUpsertIdentity } params.Column = "providerid" params.Identity = string(identity.ProviderID) case identity.Username != "": params.Column = "username" params.Identity = identity.Username default: return store.NotFoundError("", "", "") } for i, op := range upd { field := store.Field(i) if field == store.ProviderID { continue } col := identityColumns[field] if col == "" { continue } var arg interface{} switch op { case store.Clear: arg = null{} case store.Set: arg = fieldValue(field, identity) default: // ignore push and pull as they don't make sense for scalar values. continue } params.Updates = append(params.Updates, update{col, arg}) } if len(params.Updates) == 0 { tmpl = tmplIdentityID } row, err := s.driver.queryRow(tx, tmpl, params) if err != nil { return errgo.Notef(err, "cannot update identity") } if err := row.Scan(&identity.ID); err != nil { if errgo.Cause(err) == sql.ErrNoRows { return store.NotFoundError(identity.ID, identity.ProviderID, identity.Username) } if s.driver.isDuplicateFunc(err) { return store.DuplicateUsernameError(identity.Username) } return errgo.Notef(err, "cannot update identity") } if err := s.updateGroups(tx, identity.ID, upd[store.Groups], identity.Groups); err != nil { return errgo.Notef(err, "cannot update identity") } if err := s.updatePublicKeys(tx, identity.ID, upd[store.PublicKeys], identity.PublicKeys); err != nil { return errgo.Notef(err, "cannot update identity") } for k, vs := range identity.ProviderInfo { if err := s.updateProviderInfo(tx, identity.ID, k, upd[store.ProviderInfo], vs); err != nil { return errgo.Notef(err, "cannot update identity") } } for k, vs := range identity.ExtraInfo { if err := s.updateExtraInfo(tx, identity.ID, k, upd[store.ExtraInfo], vs); err != nil { return errgo.Notef(err, "cannot update identity") } } return nil } type updateSetParams struct { argBuilder Table string ID string Key string Values []interface{} } func (s *identityStore) updateSet(tx *sql.Tx, table, id, key string, op store.Operation, values []interface{}) error { if op == store.NoUpdate { return nil } params := &updateSetParams{ argBuilder: s.driver.argBuilderFunc(), Table: table, ID: id, Key: key, Values: values, } if op == store.Clear || op == store.Set { args := []interface{}{id} if key != "" { args = append(args, key) } if _, err := s.driver.exec(tx, tmplClearIdentitySet, params); err != nil { return errgo.Mask(err) } } if len(values) == 0 { return nil } // Reset the arg builder params.argBuilder = s.driver.argBuilderFunc() var tmpl tmplID switch op { case store.Set, store.Push: tmpl = tmplPushIdentitySet case store.Pull: tmpl = tmplPullIdentitySet default: return nil } if _, err := s.driver.exec(tx, tmpl, params); err != nil { return errgo.Mask(err) } return nil } func (s *identityStore) updateGroups(tx *sql.Tx, id string, op store.Operation, groups []string) error { values := make([]interface{}, len(groups)) for i, g := range groups { values[i] = g } return errgo.Mask(s.updateSet(tx, "identity_groups", id, "", op, values)) } func (s *identityStore) updatePublicKeys(tx *sql.Tx, id string, op store.Operation, pks []bakery.PublicKey) error { values := make([]interface{}, len(pks)) for i := range pks { // MarshalBinary for a key does not return an error. values[i], _ = pks[i].MarshalBinary() } return errgo.Mask(s.updateSet(tx, "identity_publickeys", id, "", op, values)) } func (s *identityStore) updateProviderInfo(tx *sql.Tx, id, key string, op store.Operation, values []string) error { vals := make([]interface{}, len(values)) for i, v := range values { vals[i] = v } return errgo.Mask(s.updateSet(tx, "identity_providerinfo", id, key, op, vals)) } func (s *identityStore) updateExtraInfo(tx *sql.Tx, id, key string, op store.Operation, values []string) error { vals := make([]interface{}, len(values)) for i, v := range values { vals[i] = v } return errgo.Mask(s.updateSet(tx, "identity_extrainfo", id, key, op, vals)) } // IdentityCounts implements store.IdentityCounts. func (s *identityStore) IdentityCounts(ctx context.Context) (map[string]int, error) { counts := make(map[string]int) rows, err := s.driver.query(s.db, tmplIdentityCounts, s.driver.argBuilderFunc()) if err != nil { return nil, errgo.Mask(err) } defer rows.Close() for rows.Next() { var idp string var count int if err := rows.Scan(&idp, &count); err != nil { return nil, errgo.Mask(err) } counts[idp] = count } return counts, errgo.Mask(rows.Err()) } type nullTime struct { Time time.Time Valid bool } // Scan implements sql.Scanner. func (n *nullTime) Scan(src interface{}) error { if src == nil { n.Time = time.Time{} n.Valid = false return nil } if t, ok := src.(time.Time); ok { n.Time = t n.Valid = true return nil } return errgo.Newf("unsupported Scan, storing driver.Value type %T into type %T", src, n) } // Value implements sqldriver.Valuer. func (n nullTime) Value() (sqldriver.Value, error) { if n.Valid { return n.Time, nil } return nil, nil } // null is value that represents a null in the SQL database. Bug // https://github.com/golang/go/issues/18716 prevents us from using a // plain nil. type null struct{} // Value implements sqldriver.Valuer. func (n null) Value() (sqldriver.Value, error) { return nil, nil } type scanner interface { Scan(dest ...interface{}) error } func scanIdentity(s scanner, identity *store.Identity) error { var name, email, owner sql.NullString var lastLogin, lastDischarge nullTime err := s.Scan( &identity.ID, &identity.ProviderID, &identity.Username, &name, &email, &lastLogin, &lastDischarge, &owner, ) if err != nil { return errgo.Mask(err, errgo.Any) } identity.Name = name.String identity.Email = email.String identity.LastLogin = lastLogin.Time identity.LastDischarge = lastDischarge.Time identity.Owner = store.ProviderIdentity(owner.String) return nil } golang-github-canonical-candid-1.12.3/store/store.go000066400000000000000000000216771457263123000223310ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package store import ( "context" "database/sql/driver" "strings" "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" errgo "gopkg.in/errgo.v1" ) // Field represents a field in an identity record. type Field int const ( ProviderID Field = iota Username Name Email Groups PublicKeys LastLogin LastDischarge ProviderInfo ExtraInfo Owner NumFields ) // An Operation represents a type of update that can be applied to an // identity record in a Store.UpdateIdentity call. type Operation byte const ( // NoUpdate makes no changes to the field. NoUpdate Operation = iota // Set overrides the value of the field with the specified value. // // For the ProviderInfo and ExtraInfo fields the values are // replaced on each specified key individually. Set // Clear removes the field from the document. // // For the ProviderInfo and ExtraInfo fields the values are // cleared on each specified key individually. Clear // Push ensures that all the values in the field are added to any // that are already present. // // For the ProviderInfo and ExtraInfo fields the new values are // added to each specified key individually. Push // Pull ensures that all the values in the field are removed from // those present. It is legal to remove values that aren't // already stored. // // For the ProviderInfo and ExtraInfo fields the values are // removed from each specified key individually. Pull ) // An Update is used in a Store.UpdateIdentity to specify how the // identity record fields should be changed. type Update [NumFields]Operation // A Comparison represents a type of comparison that can be used in a // filter in a Store.FindIdentities call. type Comparison byte const ( NoComparison Comparison = iota Equal NotEqual GreaterThan LessThan GreaterThanOrEqual LessThanOrEqual ) // A Filter is used in a Store.FindEntities call to specify how the // identities should be filtered. type Filter [NumFields]Comparison // A Sort specifies the sort order of returned identities in a call to // Store.FindIdenties. type Sort struct { Field Field Descending bool } // Store is the interface that represents the data storage mechanism for // the identity manager. type Store interface { // Context returns a context that is suitable for passing to the // other store methods. Store methods called with such a context // will be sequentially consistent; for example, a value that is // set in UpdateIdentity will immediately be available from // Identity. // // The returned close function must be called when the returned // context will no longer be used, to allow for any required // cleanup. Context(ctx context.Context) (_ context.Context, close func()) // Identity reads the given identity from persistant storage and // completes all the fields. The given identity will be matched // using the first non-zero value of ID, ProviderID or Username. // If no match can found for the given identity then an error // with the cause ErrNotFound will be returned. Identity(ctx context.Context, identity *Identity) error // FindIdentities searches for all identities that match the // given ref when the given filter has been applied. The results // will be sorted in the order specified by sort. If limit is // greater than 0 then the results will contain at most that many // identities. If skip is greater than 0 then that many results // will be skipped before those that are returned. FindIdentities(ctx context.Context, ref *Identity, filter Filter, sort []Sort, skip, limit int) ([]Identity, error) // UpdateIdentity stores the data from the given identity in // persistant storage. The identity that is updated will be the // one matching the first non-zero value of ID, ProviderID or // Username. If the ID or username does not find a match then an // error with a cause of ErrNotFound will be returned. If there // is no match for an identity specified by ProviderID and the // update specifies setting the username then a new record will // be created for the identity, in this case the assigned ID will // be written back into the given identity. // // The fields that are written to the database are dictated by // the given UpdateOperations parameter. For each updatable field // this parameter will be consulted for the type of update to // perform. If the update would result in a duplicate username // being used then an error with the cause ErrDuplicateUsername // will be returned. UpdateIdentity(ctx context.Context, identity *Identity, update Update) error // IdentityCounts returns the number of identities stored in the // store split by provider ID. IdentityCounts(ctx context.Context) (map[string]int, error) // AddMFACredential stores the specified multi-factor credential. AddMFACredential(ctx context.Context, cred MFACredential) error // RemoveMFACredential removes the multi-factor credential with the // specified username and credential name. RemoveMFACredential(ctx context.Context, providerID, name string) error // UserMFACredentials returns all multi-factor credentials for the specified user. UserMFACredentials(ctx context.Context, providerID string) ([]MFACredential, error) // IncrementMFACredentialSignCount increments the multi-factor credential sign count. IncrementMFACredentialSignCount(ctx context.Context, credentialID []byte) error // ClearMFACredentials removes all multi-factor credentials for the specified user. ClearMFACredentials(ctx context.Context, providerID string) error } // A ProviderIdentity is a provider-specific unique identity. type ProviderIdentity string // MakeProviderIdentity creates a ProviderIdentitiy from the given // provider name and provider-specific identity. func MakeProviderIdentity(provider, id string) ProviderIdentity { return ProviderIdentity(provider + ":" + id) } // Split splits a ProviderIdentity into provider and id parts. func (p ProviderIdentity) Split() (provider, id string) { s := string(p) n := strings.IndexByte(s, ':') return s[:n], s[n+1:] } // Provider returns the provider part of the identity. func (p ProviderIdentity) Provider() string { provider, _ := p.Split() return provider } // Scan implements sql.Scanner by converting a string value into a ProviderIdentity. func (p *ProviderIdentity) Scan(src interface{}) error { if s, ok := src.(string); ok { *p = ProviderIdentity(s) return nil } return errgo.Newf("unsupported Scan, storing driver.Value type %T into type %T", src, p) } // Value implements driver.Valuer. func (p ProviderIdentity) Value() (driver.Value, error) { return string(p), nil } // Identity represents an identity in the store. type Identity struct { // ID is the internal ID of the Identity, this is allocated by // the store when the identity is created. ID string // ProviderID contains the provider specific ID of the identity. ProviderID ProviderIdentity // Username contains the username of the identity. Username string // Name contains the display name of the identity. Name string // Email contains the email address of the identity. Email string // Groups contains the stored set of groups of which the identity // is a member. This should not be used by identity providers // to store group information acquired at login time (that's // what ProviderInfo is for). Groups []string // PublicKeys contains any public keys associated with the // identity. PublicKeys []bakery.PublicKey // LastLogin contains the time that the identity last logged in. LastLogin time.Time // LastDischarge contains the time that the identity last logged // in. LastDischarge time.Time // ProviderInfo contains provider specific information associated // with the identity. This field is reserved for the provider to // add any additional data the provider requires to manage the // identity. ProviderInfo map[string][]string // ExtraInfo contains extra information associated with the // identity. This field is used for any additional data that is // stored with the identity, but is not directly required by the // identity manager. ExtraInfo map[string][]string // Owner contains the ProviderIdentity of the identity that owns // this one. Owner ProviderIdentity } // MFACredential stores data about a multi-factor credential. type MFACredential struct { // ProviderID contains the provider ID of the user to which the // multi-factor credential belongs. ProviderID ProviderIdentity // Name contains a human readable name of the multi-factor credential. Name string // ID contains the ID of the multi-factor credential. ID []byte // PublicKey contains the public key of the multi-factor credential. PublicKey []byte // AttenstationType holds the attestation type. AttestationType string // AuthenticatorGUID holds the GUID of the security device used // to generate the muti-factor credential. AuthenticatorGUID []byte // AuthenticatorSignCount holds the sign count of the security device. AuthenticatorSignCount uint32 } golang-github-canonical-candid-1.12.3/store/store_test.go000066400000000000000000000007501457263123000233550ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package store_test import ( "testing" qt "github.com/frankban/quicktest" "github.com/canonical/candid/store" ) func TestProviderIdentity(t *testing.T) { c := qt.New(t) pid := store.MakeProviderIdentity("test", "test-id") c.Assert(pid, qt.Equals, store.ProviderIdentity("test:test-id")) prov, id := pid.Split() c.Assert(prov, qt.Equals, "test") c.Assert(id, qt.Equals, "test-id") } golang-github-canonical-candid-1.12.3/store/storetest/000077500000000000000000000000001457263123000226655ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/store/storetest/aclstore.go000066400000000000000000000014571457263123000250370ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package storetest import ( "context" qt "github.com/frankban/quicktest" "github.com/juju/aclstore/v2" ) // TestACLStore runs tests on the given ACLStore implementation. func TestACLStore(c *qt.C, newStore func(c *qt.C) aclstore.ACLStore) { store := newStore(c) err := store.CreateACL(context.Background(), "test", []string{"test1"}) c.Assert(err, qt.IsNil) acl, err := store.Get(context.Background(), "test") c.Assert(err, qt.IsNil) c.Assert(acl, qt.DeepEquals, []string{"test1"}) err = store.Add(context.Background(), "test", []string{"test2"}) c.Assert(err, qt.IsNil) acl, err = store.Get(context.Background(), "test") c.Assert(err, qt.IsNil) c.Assert(acl, qt.DeepEquals, []string{"test1", "test2"}) } golang-github-canonical-candid-1.12.3/store/storetest/config.go000066400000000000000000000015731457263123000244670ustar00rootroot00000000000000package storetest import ( "context" "time" qt "github.com/frankban/quicktest" "gopkg.in/yaml.v2" "github.com/canonical/candid/store" ) func TestUnmarshal(c *qt.C, configYAML string) { ctx := context.Background() var cfg struct { Storage *store.Config `yaml:"storage"` } err := yaml.Unmarshal([]byte(configYAML), &cfg) c.Assert(err, qt.IsNil) c.Assert(cfg.Storage, qt.Not(qt.IsNil)) backend, err := cfg.Storage.NewBackend() c.Assert(err, qt.IsNil) defer backend.Close() // Check that the backend can actually be used. kv, err := backend.ProviderDataStore().KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) err = kv.Set(ctx, "test-key", []byte("test-value"), time.Time{}) c.Assert(err, qt.IsNil) ctx, close := kv.Context(ctx) defer close() result, err := kv.Get(ctx, "test-key") c.Assert(err, qt.IsNil) c.Assert(string(result), qt.Equals, "test-value") } golang-github-canonical-candid-1.12.3/store/storetest/keyvalue.go000066400000000000000000000167511457263123000250530ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package storetest import ( "context" "time" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/juju/simplekv" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/store" ) type keyValueSuite struct { newStore func(c *qt.C) store.ProviderDataStore Store store.ProviderDataStore } func TestKeyValueStore(c *qt.C, newStore func(c *qt.C) store.ProviderDataStore) { qtsuite.Run(c, &keyValueSuite{ newStore: newStore, }) } func (s *keyValueSuite) Init(c *qt.C) { s.Store = s.newStore(c) } func (s *keyValueSuite) TestSet(c *qt.C) { ctx := context.Background() kv, err := s.Store.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) ctx, close := kv.Context(ctx) defer close() err = kv.Set(ctx, "test-key", []byte("test-value"), time.Time{}) c.Assert(err, qt.IsNil) result, err := kv.Get(ctx, "test-key") c.Assert(err, qt.IsNil) c.Assert(string(result), qt.Equals, "test-value") // Try again with an existing record, which might trigger different behavior. err = kv.Set(ctx, "test-key", []byte("test-value-2"), time.Time{}) c.Assert(err, qt.IsNil) result, err = kv.Get(ctx, "test-key") c.Assert(err, qt.IsNil) c.Assert(string(result), qt.Equals, "test-value-2") } func (s *keyValueSuite) TestGetNotFound(c *qt.C) { ctx := context.Background() kv, err := s.Store.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) ctx, close := kv.Context(ctx) defer close() _, err = kv.Get(ctx, "test-not-there-key") c.Assert(errgo.Cause(err), qt.Equals, simplekv.ErrNotFound) c.Assert(err, qt.ErrorMatches, "key test-not-there-key not found") } func (s *keyValueSuite) TestSetKeyOnce(c *qt.C) { ctx := context.Background() kv, err := s.Store.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) ctx, close := kv.Context(ctx) defer close() err = simplekv.SetKeyOnce(ctx, kv, "test-key", []byte("test-value"), time.Time{}) c.Assert(err, qt.IsNil) result, err := kv.Get(ctx, "test-key") c.Assert(err, qt.IsNil) c.Assert(string(result), qt.Equals, "test-value") } func (s *keyValueSuite) TestSetKeyOnceDuplicate(c *qt.C) { ctx := context.Background() kv, err := s.Store.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) ctx, close := kv.Context(ctx) defer close() err = simplekv.SetKeyOnce(ctx, kv, "test-key", []byte("test-value"), time.Time{}) c.Assert(err, qt.IsNil) err = simplekv.SetKeyOnce(ctx, kv, "test-key", []byte("test-value"), time.Time{}) c.Assert(errgo.Cause(err), qt.Equals, simplekv.ErrDuplicateKey) c.Assert(err, qt.ErrorMatches, "key test-key already exists") } func (s *keyValueSuite) TestTwoStoresForTheSameIDPCommunicate(c *qt.C) { ctx := context.Background() kv1, err := s.Store.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) kv2, err := s.Store.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) ctx1, close := kv1.Context(ctx) defer close() ctx2, close := kv2.Context(ctx) defer close() err = kv1.Set(ctx1, "test-key", []byte("test-value"), time.Time{}) c.Assert(err, qt.IsNil) v, err := kv2.Get(ctx2, "test-key") c.Assert(err, qt.IsNil) c.Assert(string(v), qt.Equals, "test-value") } func (s *keyValueSuite) TestTwoStoresForDifferentIDPsAreIndependent(c *qt.C) { ctx := context.Background() kv1, err := s.Store.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) kv2, err := s.Store.KeyValueStore(ctx, "test2") c.Assert(err, qt.IsNil) ctx1, close := kv1.Context(ctx) defer close() ctx2, close := kv2.Context(ctx) defer close() err = simplekv.SetKeyOnce(ctx1, kv1, "test-key", []byte("test-value"), time.Time{}) c.Assert(err, qt.IsNil) err = simplekv.SetKeyOnce(ctx2, kv2, "test-key", []byte("test-value-2"), time.Time{}) c.Assert(err, qt.IsNil) v, err := kv1.Get(ctx1, "test-key") c.Assert(err, qt.IsNil) c.Assert(string(v), qt.Equals, "test-value") } func (s *keyValueSuite) TestUpdateSuccessWithPreexistingKey(c *qt.C) { ctx := context.Background() kv, err := s.Store.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) err = kv.Set(ctx, "test-key", []byte("test-value"), time.Time{}) c.Assert(err, qt.IsNil) err = kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { c.Check(string(oldVal), qt.Equals, "test-value") return []byte("test-value-2"), nil }) c.Assert(err, qt.IsNil) val, err := kv.Get(ctx, "test-key") c.Assert(err, qt.IsNil) c.Assert(string(val), qt.Equals, "test-value-2") } func (s *keyValueSuite) TestUpdateSuccessWithoutPreexistingKey(c *qt.C) { ctx := context.Background() kv, err := s.Store.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) err = kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { c.Check(oldVal, qt.IsNil) return []byte("test-value"), nil }) c.Assert(err, qt.IsNil) val, err := kv.Get(ctx, "test-key") c.Assert(err, qt.IsNil) c.Assert(string(val), qt.Equals, "test-value") } func (s *keyValueSuite) TestUpdateConcurrent(c *qt.C) { ctx := context.Background() kv, err := s.Store.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) const N = 100 done := make(chan struct{}) for i := 0; i < 2; i++ { go func() { for j := 0; j < N; j++ { err := kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { time.Sleep(time.Millisecond) if oldVal == nil { return []byte{1}, nil } return []byte{oldVal[0] + 1}, nil }) c.Check(err, qt.Equals, nil) } done <- struct{}{} }() } <-done <-done val, err := kv.Get(ctx, "test-key") c.Assert(err, qt.IsNil) c.Assert(val, qt.HasLen, 1) c.Assert(int(val[0]), qt.Equals, N*2) } func (s *keyValueSuite) TestUpdateErrorWithExistingKey(c *qt.C) { ctx := context.Background() kv, err := s.Store.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) testErr := errgo.Newf("test error") err = kv.Set(ctx, "test-key", []byte("test-value"), time.Time{}) c.Assert(err, qt.IsNil) err = kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { c.Check(string(oldVal), qt.Equals, "test-value") return nil, testErr }) c.Check(errgo.Cause(err), qt.Equals, testErr) } func (s *keyValueSuite) TestUpdateErrorWithNonExistentKey(c *qt.C) { ctx := context.Background() kv, err := s.Store.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) testErr := errgo.Newf("test error") err = kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { c.Check(oldVal, qt.IsNil) return nil, testErr }) c.Check(errgo.Cause(err), qt.Equals, testErr) } func (s *keyValueSuite) TestSetNilUpdatesAsNonNil(c *qt.C) { ctx := context.Background() kv, err := s.Store.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) err = kv.Set(ctx, "test-key", nil, time.Time{}) c.Assert(err, qt.IsNil) err = kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { c.Assert(oldVal, qt.DeepEquals, []byte{}) return nil, nil }) c.Assert(err, qt.IsNil) } func (s *keyValueSuite) TestUpdateReturnNilThenUpdatesAsNonNil(c *qt.C) { ctx := context.Background() kv, err := s.Store.KeyValueStore(ctx, "test") c.Assert(err, qt.IsNil) err = kv.Set(ctx, "test-key", []byte("test-value"), time.Time{}) c.Assert(err, qt.IsNil) err = kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { c.Check(string(oldVal), qt.Equals, "test-value") return nil, nil }) c.Assert(err, qt.IsNil) err = kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { c.Check(oldVal, qt.Not(qt.IsNil)) c.Assert(oldVal, qt.DeepEquals, []byte{}) return nil, nil }) c.Assert(err, qt.IsNil) } golang-github-canonical-candid-1.12.3/store/storetest/meeting.go000066400000000000000000000103051457263123000246430ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package storetest import ( "context" "fmt" "sort" "time" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/canonical/candid/meeting" ) // meetingSuite contains a set of tests for meeting.Store // implementations. type meetingSuite struct { newStore func(c *qt.C) meeting.Store Store meeting.Store PutAtTimeFunc func(context.Context, meeting.Store, string, string, time.Time) error ctx context.Context } // TestMeetingStore tests the given store. The putAtTime function // should put an item in the store as if it had been put there // at the given time. func TestMeetingStore( c *qt.C, newStore func(c *qt.C) meeting.Store, putAtTime func(ctx context.Context, s meeting.Store, id, address string, now time.Time) error, ) { qtsuite.Run(c, &meetingSuite{ newStore: newStore, PutAtTimeFunc: putAtTime, }) } func (s *meetingSuite) Init(c *qt.C) { s.Store = s.newStore(c) ctx, close := s.Store.Context(context.Background()) s.ctx = ctx c.Defer(close) } func (s *meetingSuite) TestPutGetRemove(c *qt.C) { err := s.Store.Put(s.ctx, "x", "xaddr") c.Assert(err, qt.IsNil) err = s.Store.Put(s.ctx, "y", "yaddr") c.Assert(err, qt.IsNil) addr, err := s.Store.Get(s.ctx, "x") c.Assert(err, qt.IsNil) c.Assert(addr, qt.Equals, "xaddr") addr, err = s.Store.Get(s.ctx, "y") c.Assert(err, qt.IsNil) c.Assert(addr, qt.Equals, "yaddr") _, err = s.Store.Remove(s.ctx, "y") c.Assert(err, qt.IsNil) // Check it's idempotent. _, err = s.Store.Remove(s.ctx, "y") c.Assert(err, qt.IsNil) addr, err = s.Store.Get(s.ctx, "y") c.Assert(err, qt.ErrorMatches, "rendezvous not found, probably expired") addr, err = s.Store.Get(s.ctx, "x") c.Assert(err, qt.IsNil) c.Assert(addr, qt.Equals, "xaddr") } func (s *meetingSuite) TestRemoveNothingRemoved(c *qt.C) { now := time.Now() allIds := make(map[string]bool) for i := 0; i < 10; i++ { id := fmt.Sprint("a", i) err := s.PutAtTimeFunc(s.ctx, s.Store, id, "a", now.Add(time.Duration(-i)*time.Second)) c.Assert(err, qt.IsNil) allIds[id] = true id = fmt.Sprint("b", i) err = s.PutAtTimeFunc(s.ctx, s.Store, id, "b", now.Add(time.Duration(-i)*time.Second)) c.Assert(err, qt.IsNil) allIds[id] = true } ids, err := s.Store.RemoveOld(s.ctx, "a", now.Add(time.Duration(-11)*time.Second)) c.Assert(err, qt.IsNil) c.Assert(len(ids), qt.Equals, 0) } func (s *meetingSuite) TestRemoveOld(c *qt.C) { now := time.Now() allIds := make(map[string]bool) for i := 0; i < 10; i++ { id := fmt.Sprint("a", i) err := s.PutAtTimeFunc(s.ctx, s.Store, id, "a", now.Add(time.Duration(-i)*time.Second)) c.Assert(err, qt.IsNil) allIds[id] = true id = fmt.Sprint("b", i) err = s.PutAtTimeFunc(s.ctx, s.Store, id, "b", now.Add(time.Duration(-i)*time.Second)) c.Assert(err, qt.IsNil) allIds[id] = true } ids, err := s.Store.RemoveOld(s.ctx, "a", now.Add(-5500*time.Millisecond)) c.Assert(err, qt.IsNil) sort.Strings(ids) c.Assert(ids, qt.DeepEquals, []string{"a6", "a7", "a8", "a9"}) for _, id := range ids { _, err = s.Store.Get(s.ctx, id) c.Assert(err, qt.ErrorMatches, "rendezvous not found, probably expired") delete(allIds, id) } for id := range allIds { _, err = s.Store.Get(s.ctx, id) c.Assert(err, qt.IsNil) } ids, err = s.Store.RemoveOld(s.ctx, "", now.Add(-1500*time.Millisecond)) c.Assert(err, qt.IsNil) sort.Strings(ids) c.Assert(ids, qt.DeepEquals, []string{"a2", "a3", "a4", "a5", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9"}) for _, id := range ids { _, err = s.Store.Get(s.ctx, id) c.Assert(err, qt.ErrorMatches, "rendezvous not found, probably expired") delete(allIds, id) } for id := range allIds { _, err = s.Store.Get(s.ctx, id) c.Assert(err, qt.IsNil) } } func (s *meetingSuite) TestPutSameIDTwice(c *qt.C) { err := s.Store.Put(s.ctx, "x", "addr1") c.Assert(err, qt.IsNil) // Putting the same id should result in an error. err = s.Store.Put(s.ctx, "x", "addr2") if err == nil { c.Errorf("expected error from putting same id twice; got no error") } } func (s *meetingSuite) TestContext(c *qt.C) { ctx, close := s.Store.Context(s.ctx) defer close() c.Assert(ctx, qt.Equals, s.ctx) } golang-github-canonical-candid-1.12.3/store/storetest/store.go000066400000000000000000001123531457263123000243550ustar00rootroot00000000000000// Copyright 2017 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. // Package testing provides useful tools for testing Store // implementations. package storetest import ( "context" "fmt" "sort" "time" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" errgo "gopkg.in/errgo.v1" "github.com/canonical/candid/internal/auth" "github.com/canonical/candid/internal/candidtest" "github.com/canonical/candid/store" ) var pk1 = bakery.MustGenerateKey().Public var pk2 = bakery.MustGenerateKey().Public // storeSuite contains a set of tests for Store implementations. The // Store parameter need to be set before calling SetUpTest. type storeSuite struct { newStore func(c *qt.C) store.Store Store store.Store ctx context.Context } // TestStore runs a suite of tests on the given store implementation. func TestStore(c *qt.C, newStore func(c *qt.C) store.Store) { qtsuite.Run(c, &storeSuite{ newStore: newStore, }) } func (s *storeSuite) Init(c *qt.C) { s.Store = s.newStore(c) ctx, close := s.Store.Context(context.Background()) c.Defer(close) s.ctx = ctx } var updateIdentityTests = []struct { about string startIdentity *store.Identity updateIdentity *store.Identity update store.Update expectError string expectErrorCause error expectIdentity *store.Identity }{{ about: "new identity", updateIdentity: &store.Identity{}, update: store.Update{ store.Username: store.Set, }, expectIdentity: &store.Identity{}, }, { about: "new identity with existing username", updateIdentity: &store.Identity{ Username: "existing-user", }, update: store.Update{ store.Username: store.Set, }, expectError: `username existing-user already in use`, expectErrorCause: store.ErrDuplicateUsername, }, { about: "set username", startIdentity: &store.Identity{}, updateIdentity: &store.Identity{ Username: "bob", }, update: store.Update{ store.Username: store.Set, }, expectIdentity: &store.Identity{ Username: "bob", }, }, { about: "set username to existing username", startIdentity: &store.Identity{}, updateIdentity: &store.Identity{ Username: "existing-user", }, update: store.Update{ store.Username: store.Set, }, expectError: `username existing-user already in use`, expectErrorCause: store.ErrDuplicateUsername, expectIdentity: &store.Identity{}, }, { about: "set name", startIdentity: &store.Identity{ Name: "Test User", }, updateIdentity: &store.Identity{ Name: "Test User II", }, update: store.Update{ store.Name: store.Set, }, expectIdentity: &store.Identity{ Name: "Test User II", }, }, { about: "clear name", startIdentity: &store.Identity{ Name: "Test User", }, updateIdentity: &store.Identity{}, update: store.Update{ store.Name: store.Clear, }, expectIdentity: &store.Identity{}, }, { about: "set email", startIdentity: &store.Identity{ Email: "test@example.com", }, updateIdentity: &store.Identity{ Email: "test2@example.com", }, update: store.Update{ store.Email: store.Set, }, expectIdentity: &store.Identity{ Email: "test2@example.com", }, }, { about: "clear email", startIdentity: &store.Identity{ Email: "test@example.com", }, updateIdentity: &store.Identity{}, update: store.Update{ store.Email: store.Clear, }, expectIdentity: &store.Identity{}, }, { about: "set last discharge", startIdentity: &store.Identity{ LastDischarge: time.Date(2017, 12, 25, 0, 0, 0, 0, time.UTC), }, updateIdentity: &store.Identity{ LastDischarge: time.Date(2017, 12, 26, 0, 0, 0, 0, time.UTC), }, update: store.Update{ store.LastDischarge: store.Set, }, expectIdentity: &store.Identity{ LastDischarge: time.Date(2017, 12, 26, 0, 0, 0, 0, time.UTC), }, }, { about: "clear last discharge", startIdentity: &store.Identity{ LastDischarge: time.Date(2017, 12, 25, 0, 0, 0, 0, time.UTC), }, updateIdentity: &store.Identity{}, update: store.Update{ store.LastDischarge: store.Clear, }, expectIdentity: &store.Identity{}, }, { about: "set last login", startIdentity: &store.Identity{ LastLogin: time.Date(2017, 12, 25, 0, 0, 0, 0, time.UTC), }, updateIdentity: &store.Identity{ LastLogin: time.Date(2017, 12, 26, 0, 0, 0, 0, time.UTC), }, update: store.Update{ store.LastLogin: store.Set, }, expectIdentity: &store.Identity{ LastLogin: time.Date(2017, 12, 26, 0, 0, 0, 0, time.UTC), }, }, { about: "clear last login", startIdentity: &store.Identity{ LastLogin: time.Date(2017, 12, 25, 0, 0, 0, 0, time.UTC), }, updateIdentity: &store.Identity{}, update: store.Update{ store.LastLogin: store.Clear, }, expectIdentity: &store.Identity{}, }, { about: "set groups", startIdentity: &store.Identity{ Groups: []string{"g1", "g2"}, }, updateIdentity: &store.Identity{ Groups: []string{"g3", "g4"}, }, update: store.Update{ store.Groups: store.Set, }, expectIdentity: &store.Identity{ Groups: []string{"g3", "g4"}, }, }, { about: "set groups empty", startIdentity: &store.Identity{ Groups: []string{"g1", "g2"}, }, updateIdentity: &store.Identity{}, update: store.Update{ store.Groups: store.Set, }, expectIdentity: &store.Identity{}, }, { about: "clear groups", startIdentity: &store.Identity{ Groups: []string{"g1", "g2"}, }, updateIdentity: &store.Identity{}, update: store.Update{ store.Groups: store.Clear, }, expectIdentity: &store.Identity{}, }, { about: "push groups", startIdentity: &store.Identity{ Groups: []string{"g1", "g2"}, }, updateIdentity: &store.Identity{ Groups: []string{"g3", "g4"}, }, update: store.Update{ store.Groups: store.Push, }, expectIdentity: &store.Identity{ Groups: []string{"g1", "g2", "g3", "g4"}, }, }, { about: "push groups empty", startIdentity: &store.Identity{ Groups: []string{"g1", "g2"}, }, updateIdentity: &store.Identity{}, update: store.Update{ store.Groups: store.Push, }, expectIdentity: &store.Identity{ Groups: []string{"g1", "g2"}, }, }, { about: "pull groups", startIdentity: &store.Identity{ Groups: []string{"g1", "g2", "g3", "g4"}, }, updateIdentity: &store.Identity{ Groups: []string{"g2", "g4"}, }, update: store.Update{ store.Groups: store.Pull, }, expectIdentity: &store.Identity{ Groups: []string{"g1", "g3"}, }, }, { about: "pull groups empty", startIdentity: &store.Identity{ Groups: []string{"g1", "g2", "g3", "g4"}, }, updateIdentity: &store.Identity{}, update: store.Update{ store.Groups: store.Pull, }, expectIdentity: &store.Identity{ Groups: []string{"g1", "g2", "g3", "g4"}, }, }, { about: "set public keys", startIdentity: &store.Identity{ PublicKeys: []bakery.PublicKey{pk1}, }, updateIdentity: &store.Identity{ PublicKeys: []bakery.PublicKey{pk2}, }, update: store.Update{ store.PublicKeys: store.Set, }, expectIdentity: &store.Identity{ PublicKeys: []bakery.PublicKey{pk2}, }, }, { about: "set public keys empty", startIdentity: &store.Identity{ PublicKeys: []bakery.PublicKey{pk1}, }, updateIdentity: &store.Identity{}, update: store.Update{ store.PublicKeys: store.Set, }, expectIdentity: &store.Identity{}, }, { about: "clear public keys", startIdentity: &store.Identity{ PublicKeys: []bakery.PublicKey{pk1, pk2}, }, updateIdentity: &store.Identity{}, update: store.Update{ store.PublicKeys: store.Clear, }, expectIdentity: &store.Identity{}, }, { about: "push public keys", startIdentity: &store.Identity{ PublicKeys: []bakery.PublicKey{pk1}, }, updateIdentity: &store.Identity{ PublicKeys: []bakery.PublicKey{pk2}, }, update: store.Update{ store.PublicKeys: store.Push, }, expectIdentity: &store.Identity{ PublicKeys: []bakery.PublicKey{pk1, pk2}, }, }, { about: "push public keys empty", startIdentity: &store.Identity{ PublicKeys: []bakery.PublicKey{pk1}, }, updateIdentity: &store.Identity{}, update: store.Update{ store.PublicKeys: store.Push, }, expectIdentity: &store.Identity{ PublicKeys: []bakery.PublicKey{pk1}, }, }, { about: "pull public keys", startIdentity: &store.Identity{ PublicKeys: []bakery.PublicKey{pk1, pk2}, }, updateIdentity: &store.Identity{ PublicKeys: []bakery.PublicKey{pk1}, }, update: store.Update{ store.PublicKeys: store.Pull, }, expectIdentity: &store.Identity{ PublicKeys: []bakery.PublicKey{pk2}, }, }, { about: "pull public keys empty", startIdentity: &store.Identity{ PublicKeys: []bakery.PublicKey{pk1, pk2}, }, updateIdentity: &store.Identity{}, update: store.Update{ store.PublicKeys: store.Pull, }, expectIdentity: &store.Identity{ PublicKeys: []bakery.PublicKey{pk1, pk2}, }, }, { about: "set provider info", startIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, updateIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": {"e", "f"}, }, }, update: store.Update{ store.ProviderInfo: store.Set, }, expectIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": {"e", "f"}, "k2": {"c", "d"}, }, }, }, { about: "set provider info empty", startIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, updateIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": nil, }, }, update: store.Update{ store.ProviderInfo: store.Set, }, expectIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k2": {"c", "d"}, }, }, }, { about: "clear provider info", startIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, updateIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k2": nil, }, }, update: store.Update{ store.ProviderInfo: store.Clear, }, expectIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": {"a", "b"}, }, }, }, { about: "push provider info", startIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, updateIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": {"e", "f"}, }, }, update: store.Update{ store.ProviderInfo: store.Push, }, expectIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": {"a", "b", "e", "f"}, "k2": {"c", "d"}, }, }, }, { about: "push provider info empty", startIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, updateIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": nil, }, }, update: store.Update{ store.ProviderInfo: store.Push, }, expectIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, }, { about: "pull provider info", startIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, updateIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k2": {"c"}, }, }, update: store.Update{ store.ProviderInfo: store.Pull, }, expectIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"d"}, }, }, }, { about: "pull provider info empty", startIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, updateIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k2": nil, }, }, update: store.Update{ store.ProviderInfo: store.Pull, }, expectIdentity: &store.Identity{ ProviderInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, }, { about: "set extra info", startIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, updateIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": {"e", "f"}, }, }, update: store.Update{ store.ExtraInfo: store.Set, }, expectIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": {"e", "f"}, "k2": {"c", "d"}, }, }, }, { about: "set extra info empty", startIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, updateIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": nil, }, }, update: store.Update{ store.ExtraInfo: store.Set, }, expectIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k2": {"c", "d"}, }, }, }, { about: "clear extra info", startIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, updateIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k2": nil, }, }, update: store.Update{ store.ExtraInfo: store.Clear, }, expectIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": {"a", "b"}, }, }, }, { about: "push extra info", startIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, updateIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": {"e", "f"}, }, }, update: store.Update{ store.ExtraInfo: store.Push, }, expectIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": {"a", "b", "e", "f"}, "k2": {"c", "d"}, }, }, }, { about: "push extra info empty", startIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, updateIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": nil, }, }, update: store.Update{ store.ExtraInfo: store.Push, }, expectIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, }, { about: "pull extra info", startIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, updateIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k2": {"c"}, }, }, update: store.Update{ store.ExtraInfo: store.Pull, }, expectIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"d"}, }, }, }, { about: "pull extra info empty", startIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, updateIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k2": nil, }, }, update: store.Update{ store.ExtraInfo: store.Pull, }, expectIdentity: &store.Identity{ ExtraInfo: map[string][]string{ "k1": {"a", "b"}, "k2": {"c", "d"}, }, }, }, { about: "set owner", startIdentity: &store.Identity{}, updateIdentity: &store.Identity{ Owner: auth.AdminProviderID, }, update: store.Update{ store.Owner: store.Set, }, expectIdentity: &store.Identity{ Owner: auth.AdminProviderID, }, }, { about: "clear owner", startIdentity: &store.Identity{ Owner: auth.AdminProviderID, }, updateIdentity: &store.Identity{}, update: store.Update{ store.Owner: store.Clear, }, expectIdentity: &store.Identity{}, }, { about: "username not found", updateIdentity: &store.Identity{ Name: "Test User", }, update: store.Update{ store.Name: store.Set, }, expectError: `user .* not found`, expectErrorCause: store.ErrNotFound, }, { about: "id not found", updateIdentity: &store.Identity{ ID: "not-an-id", Name: "Test User", }, update: store.Update{ store.Name: store.Set, }, expectError: `identity "not-an-id" not found`, expectErrorCause: store.ErrNotFound, }, { about: "providerid not found", updateIdentity: &store.Identity{ ProviderID: "not-a-providerid", Name: "Test User", }, update: store.Update{ store.Name: store.Set, }, expectError: `identity "not-a-providerid" not found`, expectErrorCause: store.ErrNotFound, }, { about: "empty update", startIdentity: &store.Identity{}, updateIdentity: &store.Identity{}, update: store.Update{}, }, { about: "providerID empty update", startIdentity: &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "empty-update-user"), }, updateIdentity: &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "empty-update-user"), }, update: store.Update{}, }} func (s *storeSuite) TestUpdateIdentity(c *qt.C) { err := s.Store.UpdateIdentity( s.ctx, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "existing-user"), Username: "existing-user", }, store.Update{ store.Username: store.Set, }, ) c.Assert(err, qt.IsNil) for i, test := range updateIdentityTests { c.Run(test.about, func(c *qt.C) { username := fmt.Sprintf("user%d", i) pid := store.MakeProviderIdentity("test", username) if test.startIdentity != nil { update := store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, store.Groups: store.Set, store.PublicKeys: store.Set, store.ProviderInfo: store.Set, store.ExtraInfo: store.Set, } if test.startIdentity.ProviderID == "" { test.startIdentity.ProviderID = pid } if test.startIdentity.Username == "" { test.startIdentity.Username = username } if !test.startIdentity.LastDischarge.IsZero() { update[store.LastDischarge] = store.Set } if !test.startIdentity.LastLogin.IsZero() { update[store.LastLogin] = store.Set } err := s.Store.UpdateIdentity(s.ctx, test.startIdentity, update) c.Assert(err, qt.IsNil) } if test.updateIdentity.Username == "" && test.updateIdentity.ProviderID == "" { test.updateIdentity.Username = username } if test.update[store.Username] == store.Set { if test.updateIdentity.ProviderID == "" { test.updateIdentity.ProviderID = pid } } err := s.Store.UpdateIdentity(s.ctx, test.updateIdentity, test.update) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) if test.expectErrorCause != nil { c.Assert(errgo.Cause(err), qt.Equals, test.expectErrorCause) } } else { c.Assert(err, qt.IsNil) } if test.expectIdentity == nil { return } if test.expectIdentity.ProviderID == "" { test.expectIdentity.ProviderID = pid } if test.expectIdentity.Username == "" { test.expectIdentity.Username = username } obtained := store.Identity{ ProviderID: test.expectIdentity.ProviderID, } err = s.Store.Identity(s.ctx, &obtained) c.Assert(err, qt.IsNil) candidtest.AssertEqualIdentity(c, &obtained, test.expectIdentity) }) } } func (s *storeSuite) TestUpdateNotFoundNoQuery(c *qt.C) { identity := store.Identity{ Name: "Test User", } err := s.Store.UpdateIdentity(s.ctx, &identity, store.Update{ store.Name: store.Set, }) c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) c.Assert(err, qt.ErrorMatches, `identity not specified`) } func (s *storeSuite) TestInsertDuplicateUsername(c *qt.C) { err := s.Store.UpdateIdentity( s.ctx, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "existing-user"), Username: "existing-user", }, store.Update{ store.Username: store.Set, }, ) c.Assert(err, qt.IsNil) identity := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "test-user"), Username: "existing-user", } err = s.Store.UpdateIdentity( s.ctx, &identity, store.Update{ store.Username: store.Set, }, ) c.Assert(err, qt.ErrorMatches, `username existing-user already in use`) c.Assert(errgo.Cause(err), qt.Equals, store.ErrDuplicateUsername) identity2 := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "test-user"), } err = s.Store.Identity(s.ctx, &identity2) c.Assert(err, qt.ErrorMatches, `identity "test:test-user" not found`) c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) } func (s *storeSuite) TestUpdateIDDuplicateUsername(c *qt.C) { err := s.Store.UpdateIdentity( s.ctx, &store.Identity{ ProviderID: store.MakeProviderIdentity("test", "existing-user"), Username: "existing-user", }, store.Update{ store.Username: store.Set, }, ) c.Assert(err, qt.IsNil) identity := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "test-user"), Username: "test-user", } err = s.Store.UpdateIdentity( s.ctx, &identity, store.Update{ store.Username: store.Set, }, ) c.Assert(err, qt.IsNil) identity2 := store.Identity{ ID: identity.ID, Username: "existing-user", } err = s.Store.UpdateIdentity( s.ctx, &identity2, store.Update{ store.Username: store.Set, }, ) c.Assert(err, qt.ErrorMatches, `username existing-user already in use`) c.Assert(errgo.Cause(err), qt.Equals, store.ErrDuplicateUsername) } func (s *storeSuite) TestUpdateIDEmpty(c *qt.C) { identity := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "test-user"), Username: "test-user", } err := s.Store.UpdateIdentity( s.ctx, &identity, store.Update{ store.Username: store.Set, }, ) c.Assert(err, qt.IsNil) identity2 := store.Identity{ ID: identity.ID, } err = s.Store.UpdateIdentity( s.ctx, &identity2, store.Update{}, ) c.Assert(err, qt.IsNil) } func (s *storeSuite) TestIdentity(c *qt.C) { identity := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "test-user"), Username: "test-user", Name: "Test User", Email: "test@example.com", Groups: []string{"g1", "g2"}, PublicKeys: []bakery.PublicKey{pk1}, LastLogin: time.Date(2017, 12, 25, 0, 0, 0, 0, time.UTC), LastDischarge: time.Date(2017, 12, 25, 0, 0, 0, 0, time.UTC), ProviderInfo: map[string][]string{ "pf1": {"pf1v1", "pf1v2"}, }, ExtraInfo: map[string][]string{ "ef1": {"ef1v1", "ef1v2"}, }, Owner: store.MakeProviderIdentity("test", "test-admin"), } err := s.Store.UpdateIdentity(s.ctx, &identity, store.Update{ store.Username: store.Set, store.Name: store.Set, store.Email: store.Set, store.Groups: store.Set, store.PublicKeys: store.Set, store.LastLogin: store.Set, store.LastDischarge: store.Set, store.ProviderInfo: store.Set, store.ExtraInfo: store.Set, store.Owner: store.Set, }) c.Assert(err, qt.IsNil) identity2 := store.Identity{ ID: identity.ID, } err = s.Store.Identity(s.ctx, &identity2) c.Assert(err, qt.IsNil) c.Assert(identity2, qt.DeepEquals, identity) identity3 := store.Identity{ ProviderID: identity.ProviderID, } err = s.Store.Identity(s.ctx, &identity3) c.Assert(err, qt.IsNil) c.Assert(identity3, qt.DeepEquals, identity) identity4 := store.Identity{ Username: identity.Username, } err = s.Store.Identity(s.ctx, &identity4) c.Assert(err, qt.IsNil) c.Assert(identity4, qt.DeepEquals, identity) } func (s *storeSuite) TestIdentityNotFound(c *qt.C) { identity := store.Identity{ Username: "no-such-user", } err := s.Store.Identity(s.ctx, &identity) c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) c.Assert(err, qt.ErrorMatches, `user no-such-user not found`) } func (s *storeSuite) TestIdentityNotFoundNoQuery(c *qt.C) { identity := store.Identity{} err := s.Store.Identity(s.ctx, &identity) c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) c.Assert(err, qt.ErrorMatches, `identity not specified`) } func (s *storeSuite) TestIdentityNotFoundBadID(c *qt.C) { identity := store.Identity{ ID: "1234", } err := s.Store.Identity(s.ctx, &identity) c.Assert(errgo.Cause(err), qt.Equals, store.ErrNotFound) c.Assert(err, qt.ErrorMatches, `identity "1234" not found`) } var testIdentities = []store.Identity{{ ProviderID: store.MakeProviderIdentity("test", "test1"), Username: "test1", Name: "Test User 1", Email: "test1@example.com", Groups: []string{"g1", "g2"}, PublicKeys: []bakery.PublicKey{pk1}, LastLogin: time.Date(2017, 1, 1, 0, 0, 0, 0, time.UTC), LastDischarge: time.Date(2017, 2, 9, 0, 0, 0, 0, time.UTC), ProviderInfo: map[string][]string{ "pf1": {"pf1v1", "pf1v2"}, }, ExtraInfo: map[string][]string{ "ef1": {"ef1v1", "ef1v2"}, }, }, { ProviderID: store.MakeProviderIdentity("test", "test2"), Username: "test2", Name: "Test User 2", Email: "test2@example.com", LastLogin: time.Date(2017, 1, 2, 0, 0, 0, 0, time.UTC), LastDischarge: time.Date(2017, 2, 8, 0, 0, 0, 0, time.UTC), }, { ProviderID: store.MakeProviderIdentity("test", "test3"), Username: "test3", Name: "Test User 3", Email: "test3@example.com", LastLogin: time.Date(2017, 1, 3, 0, 0, 0, 0, time.UTC), LastDischarge: time.Date(2017, 2, 7, 0, 0, 0, 0, time.UTC), }, { ProviderID: store.MakeProviderIdentity("test", "test4"), Username: "test4", Name: "Test User 4", Email: "test4@example.com", LastLogin: time.Date(2017, 1, 4, 0, 0, 0, 0, time.UTC), LastDischarge: time.Date(2017, 2, 6, 0, 0, 0, 0, time.UTC), }, { ProviderID: store.MakeProviderIdentity("test", "test5"), Username: "test5", Name: "Test User 5", Email: "test5@example.com", LastLogin: time.Date(2017, 1, 5, 0, 0, 0, 0, time.UTC), LastDischarge: time.Date(2017, 2, 5, 0, 0, 0, 0, time.UTC), }, { ProviderID: store.MakeProviderIdentity("test", "test6"), Username: "test6", Name: "Test User 6", Email: "test6@example.com", LastLogin: time.Date(2017, 1, 6, 0, 0, 0, 0, time.UTC), LastDischarge: time.Date(2017, 2, 4, 0, 0, 0, 0, time.UTC), Owner: "test:test1", }, { ProviderID: store.MakeProviderIdentity("test", "test7"), Username: "test7", Name: "Test User 7", Email: "test9@example.com", LastLogin: time.Date(2017, 1, 7, 0, 0, 0, 0, time.UTC), LastDischarge: time.Date(2017, 2, 3, 0, 0, 0, 0, time.UTC), Owner: "test:test2", }, { ProviderID: store.MakeProviderIdentity("test", "test8"), Username: "test8", Name: "Test User 8", Email: "test8@example.com", LastLogin: time.Date(2017, 1, 8, 0, 0, 0, 0, time.UTC), LastDischarge: time.Date(2017, 2, 2, 0, 0, 0, 0, time.UTC), Owner: "test:test3", }, { ProviderID: store.MakeProviderIdentity("test", "test9"), Username: "test9", Name: "Test User 9", Email: "test9@example.com", LastLogin: time.Date(2017, 1, 9, 0, 0, 0, 0, time.UTC), LastDischarge: time.Date(2017, 2, 1, 0, 0, 0, 0, time.UTC), Owner: "test:test4", }} var findIdentitiesTests = []struct { about string ref store.Identity filter store.Filter sort []store.Sort skip int limit int expect []int }{{ about: "no matches", ref: store.Identity{ Username: "no-such-user", }, filter: store.Filter{ store.Username: store.Equal, }, }, { about: "provider ID equal", ref: store.Identity{ ProviderID: "test:test1", }, filter: store.Filter{ store.ProviderID: store.Equal, }, expect: []int{0}, }, { about: "provider ID not equal", ref: store.Identity{ ProviderID: "test:test1", }, filter: store.Filter{ store.ProviderID: store.NotEqual, }, sort: []store.Sort{{Field: store.Username}}, expect: []int{1, 2, 3, 4, 5, 6, 7, 8}, }, { about: "provider ID greater than", ref: store.Identity{ ProviderID: "test:test5", }, filter: store.Filter{ store.ProviderID: store.GreaterThan, }, sort: []store.Sort{{Field: store.Username}}, expect: []int{5, 6, 7, 8}, }, { about: "provider ID less than", ref: store.Identity{ ProviderID: "test:test5", }, filter: store.Filter{ store.ProviderID: store.LessThan, }, sort: []store.Sort{{Field: store.Username}}, expect: []int{0, 1, 2, 3}, }, { about: "provider ID greater than or equal", ref: store.Identity{ ProviderID: "test:test5", }, filter: store.Filter{ store.ProviderID: store.GreaterThanOrEqual, }, sort: []store.Sort{{Field: store.Username}}, expect: []int{4, 5, 6, 7, 8}, }, { about: "provider ID less than or equal", ref: store.Identity{ ProviderID: "test:test5", }, filter: store.Filter{ store.ProviderID: store.LessThanOrEqual, }, sort: []store.Sort{{Field: store.Username}}, expect: []int{0, 1, 2, 3, 4}, }, { about: "username equal", ref: store.Identity{ Username: "test1", }, filter: store.Filter{ store.Username: store.Equal, }, expect: []int{0}, }, { about: "username not equal", ref: store.Identity{ Username: "test1", }, filter: store.Filter{ store.Username: store.NotEqual, }, sort: []store.Sort{{Field: store.Username}}, expect: []int{1, 2, 3, 4, 5, 6, 7, 8}, }, { about: "username greater than", ref: store.Identity{ Username: "test5", }, filter: store.Filter{ store.Username: store.GreaterThan, }, sort: []store.Sort{{Field: store.Username}}, expect: []int{5, 6, 7, 8}, }, { about: "username less than", ref: store.Identity{ Username: "test5", }, filter: store.Filter{ store.Username: store.LessThan, }, sort: []store.Sort{{Field: store.Username}}, expect: []int{0, 1, 2, 3}, }, { about: "username greater than or equal", ref: store.Identity{ Username: "test5", }, filter: store.Filter{ store.Username: store.GreaterThanOrEqual, }, sort: []store.Sort{{Field: store.Username}}, expect: []int{4, 5, 6, 7, 8}, }, { about: "username less than or equal", ref: store.Identity{ Username: "test5", }, filter: store.Filter{ store.Username: store.LessThanOrEqual, }, sort: []store.Sort{{Field: store.Username}}, expect: []int{0, 1, 2, 3, 4}, }, { about: "name equal", ref: store.Identity{ Name: "Test User 1", }, filter: store.Filter{ store.Name: store.Equal, }, expect: []int{0}, }, { about: "name not equal", ref: store.Identity{ Name: "Test User 1", }, filter: store.Filter{ store.Name: store.NotEqual, }, sort: []store.Sort{{Field: store.Username}}, expect: []int{1, 2, 3, 4, 5, 6, 7, 8}, }, { about: "name greater than", ref: store.Identity{ Name: "Test User 5", }, filter: store.Filter{ store.Name: store.GreaterThan, }, sort: []store.Sort{{Field: store.Username}}, expect: []int{5, 6, 7, 8}, }, { about: "name less than", ref: store.Identity{ Name: "Test User 5", }, filter: store.Filter{ store.Name: store.LessThan, }, sort: []store.Sort{{Field: store.Username}}, expect: []int{0, 1, 2, 3}, }, { about: "name greater than or equal", ref: store.Identity{ Name: "Test User 5", }, filter: store.Filter{ store.Name: store.GreaterThanOrEqual, }, sort: []store.Sort{{Field: store.Username}}, expect: []int{4, 5, 6, 7, 8}, }, { about: "name less than or equal", ref: store.Identity{ Name: "Test User 5", }, filter: store.Filter{ store.Name: store.LessThanOrEqual, }, sort: []store.Sort{{Field: store.Username}}, expect: []int{0, 1, 2, 3, 4}, }, { about: "match email", ref: store.Identity{ Email: "test3@example.com", }, filter: store.Filter{ store.Email: store.Equal, }, expect: []int{2}, }, { about: "match last login", ref: store.Identity{ LastLogin: time.Date(2017, 1, 4, 0, 0, 0, 0, time.UTC), }, filter: store.Filter{ store.LastLogin: store.Equal, }, expect: []int{3}, }, { about: "match last discharge", ref: store.Identity{ LastDischarge: time.Date(2017, 2, 5, 0, 0, 0, 0, time.UTC), }, filter: store.Filter{ store.LastDischarge: store.Equal, }, expect: []int{4}, }, { about: "match less than", ref: store.Identity{ Username: "test3", }, filter: store.Filter{ store.Username: store.LessThan, }, expect: []int{0, 1}, }, { about: "match less than or equal to", ref: store.Identity{ Username: "test3", }, filter: store.Filter{ store.Username: store.LessThanOrEqual, }, expect: []int{0, 1, 2}, }, { about: "match greater than", ref: store.Identity{ Username: "test7", }, filter: store.Filter{ store.Username: store.GreaterThan, }, expect: []int{7, 8}, }, { about: "match greater than or equal to", ref: store.Identity{ Username: "test7", }, filter: store.Filter{ store.Username: store.GreaterThanOrEqual, }, expect: []int{6, 7, 8}, }, { about: "match not equal to", ref: store.Identity{ Username: "test7", }, filter: store.Filter{ store.Username: store.NotEqual, }, expect: []int{0, 1, 2, 3, 4, 5, 7, 8}, }, { about: "sort last login - descending", sort: []store.Sort{{ Field: store.LastLogin, Descending: true, }}, expect: []int{8, 7, 6, 5, 4, 3, 2, 1, 0}, }, { about: "sort last discharge - ascending", sort: []store.Sort{{ Field: store.LastDischarge, }}, expect: []int{8, 7, 6, 5, 4, 3, 2, 1, 0}, }, { about: "with skip and limit", sort: []store.Sort{{ Field: store.Username, Descending: true, }}, skip: 2, limit: 3, expect: []int{6, 5, 4}, }, { about: "match owner", ref: store.Identity{ Owner: "test:test1", }, filter: store.Filter{ store.Owner: store.Equal, }, expect: []int{5}, }} func (s *storeSuite) TestFindIdentities(c *qt.C) { for i := range testIdentities { var update store.Update if testIdentities[i].Username != "" { update[store.Username] = store.Set } if testIdentities[i].Name != "" { update[store.Name] = store.Set } if testIdentities[i].Email != "" { update[store.Email] = store.Set } if len(testIdentities[i].Groups) > 0 { update[store.Groups] = store.Set } if len(testIdentities[i].PublicKeys) > 0 { update[store.PublicKeys] = store.Set } if !testIdentities[i].LastLogin.IsZero() { update[store.LastLogin] = store.Set } if !testIdentities[i].LastDischarge.IsZero() { update[store.LastDischarge] = store.Set } if len(testIdentities[i].ProviderInfo) > 0 { update[store.ProviderInfo] = store.Set } if len(testIdentities[i].ExtraInfo) > 0 { update[store.ExtraInfo] = store.Set } if testIdentities[i].Owner != "" { update[store.Owner] = store.Set } err := s.Store.UpdateIdentity(s.ctx, &testIdentities[i], update) c.Assert(err, qt.IsNil) } for i, test := range findIdentitiesTests { c.Logf("%d. %s", i, test.about) identities, err := s.Store.FindIdentities(s.ctx, &test.ref, test.filter, test.sort, test.skip, test.limit) c.Assert(err, qt.IsNil) c.Assert(len(identities), qt.Equals, len(test.expect)) for i, identity := range identities { candidtest.AssertEqualIdentity(c, &identity, &testIdentities[test.expect[i]]) } } } func (s *storeSuite) TestIdentityCounts(c *qt.C) { idps := []string{"a", "b", "c", "a", "b", "a"} for i, idp := range idps { username := fmt.Sprintf("user%d", i) err := s.Store.UpdateIdentity(s.ctx, &store.Identity{ ProviderID: store.MakeProviderIdentity(idp, username), Username: username, }, store.Update{ store.Username: store.Set, }) c.Assert(err, qt.IsNil) } counts, err := s.Store.IdentityCounts(s.ctx) c.Assert(err, qt.IsNil) c.Assert(counts, qt.DeepEquals, map[string]int{ "a": 3, "b": 2, "c": 1, }) } func (s *storeSuite) TestUserCredentials(c *qt.C) { // add an identity identity := store.Identity{ ProviderID: store.MakeProviderIdentity("test", "existing-user"), Username: "test-user", } err := s.Store.UpdateIdentity( s.ctx, &identity, store.Update{ store.Username: store.Set, }, ) c.Assert(err, qt.IsNil) err = s.Store.ClearMFACredentials(s.ctx, string(identity.ProviderID)) c.Assert(err, qt.Equals, nil) // no credentials exist for the created user creds, err := s.Store.UserMFACredentials(s.ctx, identity.ID) c.Assert(err, qt.IsNil) c.Assert(creds, qt.DeepEquals, []store.MFACredential(nil)) // add a credential for the created user cred := store.MFACredential{ ID: []byte("test id 1"), ProviderID: identity.ProviderID, Name: "test credential 1", PublicKey: []byte("public key 1"), AttestationType: "test attestation type", AuthenticatorGUID: []byte("guid 1"), AuthenticatorSignCount: 1, } err = s.Store.AddMFACredential(s.ctx, cred) c.Assert(err, qt.IsNil) // try fetching credentials for the test user creds, err = s.Store.UserMFACredentials(s.ctx, string(identity.ProviderID)) c.Assert(err, qt.IsNil) c.Assert(creds, qt.DeepEquals, []store.MFACredential{cred}) // try adding a credential with a duplicate name cred1 := cred cred1.ID = []byte("test id 2") err = s.Store.AddMFACredential(s.ctx, cred1) c.Assert(errgo.Cause(err), qt.Equals, store.ErrDuplicateCredential) cred2 := store.MFACredential{ ID: []byte("test id 3"), ProviderID: identity.ProviderID, Name: "test credential 2", PublicKey: []byte("public key 2"), AttestationType: "test attestation type", AuthenticatorGUID: []byte("guid 2"), AuthenticatorSignCount: 2, } err = s.Store.AddMFACredential(s.ctx, cred2) c.Assert(err, qt.IsNil) // try fetching credentials for the test user creds, err = s.Store.UserMFACredentials(s.ctx, string(identity.ProviderID)) c.Assert(err, qt.IsNil) sort.Slice(creds, func(i, j int) bool { return creds[i].Name < creds[j].Name }) c.Assert(creds, qt.DeepEquals, []store.MFACredential{cred, cred2}) err = s.Store.IncrementMFACredentialSignCount(s.ctx, cred.ID) c.Assert(err, qt.IsNil) err = s.Store.IncrementMFACredentialSignCount(s.ctx, cred2.ID) c.Assert(err, qt.IsNil) err = s.Store.IncrementMFACredentialSignCount(s.ctx, cred2.ID) c.Assert(err, qt.IsNil) // fetch the user credentials and verify sign counts match // expected values cred.AuthenticatorSignCount = 2 cred2.AuthenticatorSignCount = 4 creds, err = s.Store.UserMFACredentials(s.ctx, string(identity.ProviderID)) c.Assert(err, qt.IsNil) sort.Slice(creds, func(i, j int) bool { return creds[i].Name < creds[j].Name }) c.Assert(creds, qt.DeepEquals, []store.MFACredential{cred, cred2}) err = s.Store.ClearMFACredentials(s.ctx, string(identity.ProviderID)) c.Assert(err, qt.Equals, nil) creds, err = s.Store.UserMFACredentials(s.ctx, string(identity.ProviderID)) c.Assert(err, qt.IsNil) c.Assert(creds, qt.HasLen, 0) } golang-github-canonical-candid-1.12.3/templates/000077500000000000000000000000001457263123000214735ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/templates/authentication-required000066400000000000000000000060461457263123000262610ustar00rootroot00000000000000 Candid - Authentication Required
{{if .BrandLogoLocation}} {{end}}
golang-github-canonical-candid-1.12.3/templates/login000066400000000000000000000025221457263123000225270ustar00rootroot00000000000000 Candid - Login
{{if .BrandLogoLocation}} {{end}}

You're logged in as {{.Username}}

You can now close this window.


{{if .ManageURL}} {{end}}
golang-github-canonical-candid-1.12.3/templates/login-form000066400000000000000000000036561457263123000235010ustar00rootroot00000000000000 Candid - Login
{{if .BrandLogoLocation}} {{end}}
golang-github-canonical-candid-1.12.3/templates/mfa000066400000000000000000000164001457263123000221620ustar00rootroot00000000000000 Candid - MFA Registration
{{if .BrandLogoLocation}} {{end}}

Security keys

{{if .MustRegister}}

Identity provider requires you to register a 2FA security key.

{{else}}

Please complete the MFA login.

{{end}}
golang-github-canonical-candid-1.12.3/templates/mfa-manage000066400000000000000000000152171457263123000234150ustar00rootroot00000000000000 Candid - MFA Registration
{{if .BrandLogoLocation}} {{end}}

Manage security keys

{{.Note}}

Your registered security keys

    {{range .Credentials}}
    {{.Name}}
    {{end}}

You are logged in. You can close this window.
golang-github-canonical-candid-1.12.3/templates/register000066400000000000000000000054211457263123000232440ustar00rootroot00000000000000 Candid - User Registration
{{if .BrandLogoLocation}} {{end}}

User Registration


{{if .Error}}

Error:{{.Error}}

{{end}}

@{{.Domain}}



Back
golang-github-canonical-candid-1.12.3/templates/remove-credential-confirmation000066400000000000000000000054441457263123000275200ustar00rootroot00000000000000 Candid - MFA Registration
{{if .BrandLogoLocation}} {{end}}

Remove security key

You are going to remove the following security key:

{{.Name}}

Are you sure?


Cancel
golang-github-canonical-candid-1.12.3/version/000077500000000000000000000000001457263123000211625ustar00rootroot00000000000000golang-github-canonical-candid-1.12.3/version/init.go.tmpl000066400000000000000000000006631457263123000234340ustar00rootroot00000000000000package version // init is used to update the version info with the correct details for // the current build. It is expected that an appropriate build script // or Makefile will create a new init.go file based on this template // using a command like the following: // // gofmt -r "unknownVersion -> Version{GitCommit: \"${GIT_COMMIT}\", Version: \"${VERSION}\",}" init.go.tmpl > init.go func init() { VersionInfo = unknownVersion } golang-github-canonical-candid-1.12.3/version/version.go000066400000000000000000000013221457263123000231740ustar00rootroot00000000000000// Copyright 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package version // Version describes the current version of the code being run. type Version struct { GitCommit string Version string } // VersionInfo is a variable representing the version of the currently // executing code. Builds of the system where the version information // is required must arrange to provide the correct values for this // variable. One possible way to do this is to create an init() function // that updates this variable, please see init.go.tmpl to see an example. var VersionInfo = unknownVersion var unknownVersion = Version{ GitCommit: "unknown git commit", Version: "unknown version", }