mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-05-19 18:50:14 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9db12322ea | |||
| ce7963b3f7 | |||
| deb799eff3 |
@@ -28,18 +28,6 @@ jobs:
|
|||||||
- name: Go dependencies
|
- name: Go dependencies
|
||||||
run: go mod download
|
run: go mod download
|
||||||
|
|
||||||
- name: Setup sqlc
|
|
||||||
uses: sqlc-dev/setup-sqlc@v4
|
|
||||||
with:
|
|
||||||
sqlc-version: "1.31.1"
|
|
||||||
|
|
||||||
- name: Check codegen is up to date
|
|
||||||
run: |
|
|
||||||
sqlc generate
|
|
||||||
go generate ./internal/repository/...
|
|
||||||
git diff --exit-code -- internal/repository/
|
|
||||||
git status --porcelain -- internal/repository/ | grep -q . && echo "untracked files in internal/repository/" && exit 1 || true
|
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
run: pnpm ci
|
run: pnpm ci
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
@@ -7,17 +7,15 @@
|
|||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
software and other kinds of works.
|
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
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
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
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
software for all its users.
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
@@ -26,44 +24,34 @@ 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
|
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.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
Developers that use our General Public Licenses protect your rights
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
you this License which gives you legal permission to copy, distribute
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
and/or modify the software.
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
A secondary benefit of defending all users' freedom is that
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
improvements made in alternate versions of the program, if they
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
receive widespread use, become available for other developers to
|
||||||
or can get the source code. And you must show them these terms so they
|
incorporate. Many developers of free software are heartened and
|
||||||
know their rights.
|
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.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
The GNU Affero General Public License is designed specifically to
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
ensure that, in such cases, the modified source code becomes available
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
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.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
An older license, called the Affero General Public License and
|
||||||
that there is no warranty for this free software. For both users' and
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
changed, so that their problems will not be attributed erroneously to
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
authors of previous versions.
|
this license.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
@@ -72,7 +60,7 @@ modification follow.
|
|||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"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
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
@@ -549,35 +537,45 @@ 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
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
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
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
under version 3 of the GNU General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
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,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the special requirements of the GNU Affero General Public License,
|
but the work with which it is combined will remain governed by version
|
||||||
section 13, concerning interaction through a network will apply to the
|
3 of the GNU General Public License.
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU General Public License from time to time. Such new versions will
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU General
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU General Public License, you may choose any version ever published
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If your software can interact with users remotely through a computer
|
||||||
notice like this when it starts in an interactive mode:
|
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
|
||||||
<program> Copyright (C) <year> <name of author>
|
interface could display a "Source" link that leads users to an archive
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
of the code. There are many ways you could offer source, and different
|
||||||
This is free software, and you are welcome to redistribute it
|
solutions will be better for different programs; see section 13 for the
|
||||||
under certain conditions; type `show c' for details.
|
specific requirements.
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
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.
|
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 GPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
@@ -85,4 +85,3 @@ sql:
|
|||||||
# Go gen
|
# Go gen
|
||||||
generate:
|
generate:
|
||||||
go run ./gen
|
go run ./gen
|
||||||
go generate ./internal/repository/...
|
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ Tinyauth is the simplest and tiniest authentication and authorization server you
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> This is the main development branch. For the latest stable release, see the [documentation](https://tinyauth.app) or the latest stable tag.
|
> This is the main development branch. For the latest stable release, see the [documentation](https://tinyauth.app) or the latest stable tag.
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Tinyauth is in the process of migrating to the new [tinyauthapp](https://github.com/tinyauthapp) organization. The organization **is official** and it will host all of the Tinyauth related repositories in the future.
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
You can get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started). There is also an available [docker-compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities (keep in mind that this file lives in the development branch so it may have updates that are not yet released).
|
You can get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started). There is also an available [docker-compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities (keep in mind that this file lives in the development branch so it may have updates that are not yet released).
|
||||||
@@ -59,7 +56,7 @@ If you like, you can help translate Tinyauth into more languages by visiting the
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions. For more information about the license check the [license](./LICENSE) file.
|
Tinyauth is licensed under the GNU Affero General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) AGPL-licensed code must also be made available under the AGPL along with build & install instructions. If you run a modified version over a network, you must also make the source available to the users of that service. For more information about the license check the [license](LICENSE) file.
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.6
|
image: traefik:v3.6
|
||||||
command: --api.insecure=true --providers.docker --entrypoints.web.address=:80 --entrypoints.websecure.address=:443
|
command: --api.insecure=true --providers.docker
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
- 443:443
|
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
@@ -26,8 +25,6 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
traefik.enable: true
|
traefik.enable: true
|
||||||
traefik.http.routers.tinyauth.rule: Host(`tinyauth.127.0.0.1.sslip.io`)
|
traefik.http.routers.tinyauth.rule: Host(`tinyauth.127.0.0.1.sslip.io`)
|
||||||
traefik.http.routers.tinyauth.entrypoints: websecure
|
|
||||||
traefik.http.routers.tinyauth.tls: true
|
|
||||||
|
|
||||||
tinyauth-backend:
|
tinyauth-backend:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { Navigate } from "react-router";
|
|||||||
import { useUserContext } from "./context/user-context";
|
import { useUserContext } from "./context/user-context";
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const { auth } = useUserContext();
|
const { isLoggedIn } = useUserContext();
|
||||||
|
|
||||||
if (auth.authenticated) {
|
if (isLoggedIn) {
|
||||||
return <Navigate to="/logout" replace />;
|
return <Navigate to="/logout" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ import { DomainWarning } from "../domain-warning/domain-warning";
|
|||||||
import { ThemeToggle } from "../theme-toggle/theme-toggle";
|
import { ThemeToggle } from "../theme-toggle/theme-toggle";
|
||||||
|
|
||||||
const BaseLayout = ({ children }: { children: React.ReactNode }) => {
|
const BaseLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
const { ui } = useAppContext();
|
const { backgroundImage, title } = useAppContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = ui.title;
|
document.title = title;
|
||||||
}, [ui.title]);
|
}, [title]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-col justify-center items-center min-h-svh px-4"
|
className="flex flex-col justify-center items-center min-h-svh px-4"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${ui.backgroundImage})`,
|
backgroundImage: `url(${backgroundImage})`,
|
||||||
backgroundSize: "cover",
|
backgroundSize: "cover",
|
||||||
backgroundPosition: "center",
|
backgroundPosition: "center",
|
||||||
}}
|
}}
|
||||||
@@ -31,7 +31,7 @@ const BaseLayout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Layout = () => {
|
export const Layout = () => {
|
||||||
const { app, ui } = useAppContext();
|
const { appUrl, warningsEnabled } = useAppContext();
|
||||||
const [ignoreDomainWarning, setIgnoreDomainWarning] = useState(() => {
|
const [ignoreDomainWarning, setIgnoreDomainWarning] = useState(() => {
|
||||||
return window.sessionStorage.getItem("ignoreDomainWarning") === "true";
|
return window.sessionStorage.getItem("ignoreDomainWarning") === "true";
|
||||||
});
|
});
|
||||||
@@ -42,15 +42,11 @@ export const Layout = () => {
|
|||||||
setIgnoreDomainWarning(true);
|
setIgnoreDomainWarning(true);
|
||||||
}, [setIgnoreDomainWarning]);
|
}, [setIgnoreDomainWarning]);
|
||||||
|
|
||||||
if (
|
if (!ignoreDomainWarning && warningsEnabled && appUrl !== currentUrl) {
|
||||||
!ignoreDomainWarning &&
|
|
||||||
ui.warningsEnabled &&
|
|
||||||
!app.trustedDomains.includes(currentUrl)
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<DomainWarning
|
<DomainWarning
|
||||||
appUrl={app.appUrl}
|
appUrl={appUrl}
|
||||||
currentUrl={currentUrl}
|
currentUrl={currentUrl}
|
||||||
onClick={() => handleIgnore()}
|
onClick={() => handleIgnore()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -80,17 +80,5 @@
|
|||||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
"groupsScopeName": "Groups",
|
"groupsScopeName": "Groups",
|
||||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||||
"backToLoginButton": "Back to login",
|
"backToLoginButton": "Back to login"
|
||||||
"phoneScopeName": "Phone",
|
|
||||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
|
||||||
"addressScopeName": "Address",
|
|
||||||
"addressScopeDescription": "Allows the app to access your address.",
|
|
||||||
"loginTailscaleTitle": "Continue with Tailscale",
|
|
||||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
|
||||||
"loginTailscaleDeviceName": "Device name:",
|
|
||||||
"loginTailscaleSubmit": "Continue with Tailscale",
|
|
||||||
"loginTailscaleOtherMethod": "Login with another method",
|
|
||||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
|
||||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
|
||||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,13 +84,5 @@
|
|||||||
"phoneScopeName": "Phone",
|
"phoneScopeName": "Phone",
|
||||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||||
"addressScopeName": "Address",
|
"addressScopeName": "Address",
|
||||||
"addressScopeDescription": "Allows the app to access your address.",
|
"addressScopeDescription": "Allows the app to access your address."
|
||||||
"loginTailscaleTitle": "Continue with Tailscale",
|
|
||||||
"loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
|
||||||
"loginTailscaleDeviceName": "Device name:",
|
|
||||||
"loginTailscaleSubmit": "Continue with Tailscale",
|
|
||||||
"loginTailscaleOtherMethod": "Login with another method",
|
|
||||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
|
||||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
|
||||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const createScopeMap = (t: TFunction<"translation", undefined>): Scope[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AuthorizePage = () => {
|
export const AuthorizePage = () => {
|
||||||
const { auth } = useUserContext();
|
const { isLoggedIn } = useUserContext();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -127,7 +127,7 @@ export const AuthorizePage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!auth.authenticated) {
|
if (!isLoggedIn) {
|
||||||
return <Navigate to={`/login?${oidcParams.compiled}`} replace />;
|
return <Navigate to={`/login?${oidcParams.compiled}`} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|||||||
import { useRedirectUri } from "@/lib/hooks/redirect-uri";
|
import { useRedirectUri } from "@/lib/hooks/redirect-uri";
|
||||||
|
|
||||||
export const ContinuePage = () => {
|
export const ContinuePage = () => {
|
||||||
const { app, ui } = useAppContext();
|
const { cookieDomain, warningsEnabled } = useAppContext();
|
||||||
const { auth } = useUserContext();
|
const { isLoggedIn } = useUserContext();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -29,18 +29,17 @@ export const ContinuePage = () => {
|
|||||||
|
|
||||||
const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri(
|
const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri(
|
||||||
redirectUri,
|
redirectUri,
|
||||||
app.cookieDomain,
|
cookieDomain,
|
||||||
);
|
);
|
||||||
|
|
||||||
const urlHref = url?.href;
|
const urlHref = url?.href;
|
||||||
|
|
||||||
const hasValidRedirect = valid && allowedProto;
|
const hasValidRedirect = valid && allowedProto;
|
||||||
const showUntrustedWarning =
|
const showUntrustedWarning = hasValidRedirect && !trusted && warningsEnabled;
|
||||||
hasValidRedirect && !trusted && ui.warningsEnabled;
|
|
||||||
const showInsecureWarning =
|
const showInsecureWarning =
|
||||||
hasValidRedirect && httpsDowngrade && ui.warningsEnabled;
|
hasValidRedirect && httpsDowngrade && warningsEnabled;
|
||||||
const shouldAutoRedirect =
|
const shouldAutoRedirect =
|
||||||
auth.authenticated &&
|
isLoggedIn &&
|
||||||
hasValidRedirect &&
|
hasValidRedirect &&
|
||||||
!showUntrustedWarning &&
|
!showUntrustedWarning &&
|
||||||
!showInsecureWarning;
|
!showInsecureWarning;
|
||||||
@@ -78,7 +77,7 @@ export const ContinuePage = () => {
|
|||||||
};
|
};
|
||||||
}, [shouldAutoRedirect, redirectToTarget]);
|
}, [shouldAutoRedirect, redirectToTarget]);
|
||||||
|
|
||||||
if (!auth.authenticated) {
|
if (!isLoggedIn) {
|
||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
to={`/login${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
|
to={`/login${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
|
||||||
@@ -105,7 +104,7 @@ export const ContinuePage = () => {
|
|||||||
components={{
|
components={{
|
||||||
code: <code />,
|
code: <code />,
|
||||||
}}
|
}}
|
||||||
values={{ cookieDomain: app.cookieDomain }}
|
values={{ cookieDomain }}
|
||||||
shouldUnescape={true}
|
shouldUnescape={true}
|
||||||
/>
|
/>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import Markdown from "react-markdown";
|
|||||||
import { useLocation } from "react-router";
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
export const ForgotPasswordPage = () => {
|
export const ForgotPasswordPage = () => {
|
||||||
const { ui } = useAppContext();
|
const { forgotPasswordMessage } = useAppContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const searchParams = new URLSearchParams(search);
|
const searchParams = new URLSearchParams(search);
|
||||||
@@ -26,8 +26,8 @@ export const ForgotPasswordPage = () => {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
<Markdown>
|
<Markdown>
|
||||||
{ui.forgotPasswordMessage !== ""
|
{forgotPasswordMessage !== ""
|
||||||
? ui.forgotPasswordMessage
|
? forgotPasswordMessage
|
||||||
: t("forgotPasswordMessage")}
|
: t("forgotPasswordMessage")}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
|||||||
@@ -36,17 +36,12 @@ const iconMap: Record<string, React.ReactNode> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const LoginPage = () => {
|
export const LoginPage = () => {
|
||||||
const { auth, tailscale } = useUserContext();
|
const { isLoggedIn } = useUserContext();
|
||||||
const {
|
const { providers, title, oauthAutoRedirect } = useAppContext();
|
||||||
ui,
|
|
||||||
oauth,
|
|
||||||
auth: { providers },
|
|
||||||
} = useAppContext();
|
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [showRedirectButton, setShowRedirectButton] = useState(false);
|
const [showRedirectButton, setShowRedirectButton] = useState(false);
|
||||||
const [useTailscale, setUseTailscale] = useState(tailscale.nodeName !== "");
|
|
||||||
|
|
||||||
const hasAutoRedirectedRef = useRef(false);
|
const hasAutoRedirectedRef = useRef(false);
|
||||||
|
|
||||||
@@ -60,7 +55,7 @@ export const LoginPage = () => {
|
|||||||
const oidcParams = useOIDCParams(searchParams);
|
const oidcParams = useOIDCParams(searchParams);
|
||||||
|
|
||||||
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
|
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
|
||||||
providers.find((provider) => provider.id === oauth.autoRedirect) !==
|
providers.find((provider) => provider.id === oauthAutoRedirect) !==
|
||||||
undefined && redirectUri !== undefined,
|
undefined && redirectUri !== undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -153,47 +148,21 @@ export const LoginPage = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: tailscaleMutate, isPending: tailscaleIsPending } =
|
|
||||||
useMutation({
|
|
||||||
mutationFn: () => axios.post("/api/user/tailscale"),
|
|
||||||
mutationKey: ["tailscale"],
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success(t("loginSuccessTitle"), {
|
|
||||||
description: t("loginTailscaleSuccess"),
|
|
||||||
});
|
|
||||||
|
|
||||||
redirectTimer.current = window.setTimeout(() => {
|
|
||||||
if (oidcParams.isOidc) {
|
|
||||||
window.location.replace(`/authorize?${oidcParams.compiled}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.location.replace(
|
|
||||||
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
|
|
||||||
);
|
|
||||||
}, 500);
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
toast.error(t("loginFailTitle"), {
|
|
||||||
description: t("loginTailscaleFail"),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!auth.authenticated &&
|
!isLoggedIn &&
|
||||||
isOauthAutoRedirect &&
|
isOauthAutoRedirect &&
|
||||||
!hasAutoRedirectedRef.current &&
|
!hasAutoRedirectedRef.current &&
|
||||||
redirectUri !== undefined
|
redirectUri !== undefined
|
||||||
) {
|
) {
|
||||||
hasAutoRedirectedRef.current = true;
|
hasAutoRedirectedRef.current = true;
|
||||||
oauthMutate(oauth.autoRedirect);
|
oauthMutate(oauthAutoRedirect);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
auth.authenticated,
|
isLoggedIn,
|
||||||
oauthMutate,
|
oauthMutate,
|
||||||
hasAutoRedirectedRef,
|
hasAutoRedirectedRef,
|
||||||
oauth.autoRedirect,
|
oauthAutoRedirect,
|
||||||
isOauthAutoRedirect,
|
isOauthAutoRedirect,
|
||||||
redirectUri,
|
redirectUri,
|
||||||
]);
|
]);
|
||||||
@@ -210,11 +179,11 @@ export const LoginPage = () => {
|
|||||||
};
|
};
|
||||||
}, [redirectTimer, redirectButtonTimer]);
|
}, [redirectTimer, redirectButtonTimer]);
|
||||||
|
|
||||||
if (auth.authenticated && oidcParams.isOidc) {
|
if (isLoggedIn && oidcParams.isOidc) {
|
||||||
return <Navigate to={`/authorize?${oidcParams.compiled}`} replace />;
|
return <Navigate to={`/authorize?${oidcParams.compiled}`} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.authenticated && redirectUri !== undefined) {
|
if (isLoggedIn && redirectUri !== undefined) {
|
||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
to={`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
|
to={`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
|
||||||
@@ -223,7 +192,7 @@ export const LoginPage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.authenticated) {
|
if (isLoggedIn) {
|
||||||
return <Navigate to="/logout" replace />;
|
return <Navigate to="/logout" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,49 +228,10 @@ export const LoginPage = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useTailscale) {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="gap-3">
|
|
||||||
<TailscaleIcon className="mx-auto h-8 w-8" />
|
|
||||||
<CardTitle className="text-center text-xl">
|
|
||||||
{t("loginTailscaleTitle")}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
|
||||||
<div className="text-muted-foreground text-sm">
|
|
||||||
{t("loginTailscaleDescription")}
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground text-sm">
|
|
||||||
{t("loginTailscaleDeviceName")} <code>{tailscale.nodeName}</code>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex flex-col items-stretch gap-3">
|
|
||||||
<Button
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => tailscaleMutate()}
|
|
||||||
loading={tailscaleIsPending}
|
|
||||||
>
|
|
||||||
{t("loginTailscaleSubmit")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="w-full"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => setUseTailscale(false)}
|
|
||||||
disabled={tailscaleIsPending}
|
|
||||||
>
|
|
||||||
{t("loginTailscaleOtherMethod")}
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="gap-1.5">
|
<CardHeader className="gap-1.5">
|
||||||
<CardTitle className="text-center text-xl">{ui.title}</CardTitle>
|
<CardTitle className="text-center text-xl">{title}</CardTitle>
|
||||||
{providers.length > 0 && (
|
{providers.length > 0 && (
|
||||||
<CardDescription className="text-center">
|
<CardDescription className="text-center">
|
||||||
{oauthProviders.length !== 0
|
{oauthProviders.length !== 0
|
||||||
|
|||||||
@@ -13,11 +13,9 @@ import { useEffect, useRef } from "react";
|
|||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { Navigate } from "react-router";
|
import { Navigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type UseMutationResult } from "@tanstack/react-query";
|
|
||||||
import { type AxiosResponse } from "axios";
|
|
||||||
|
|
||||||
export const LogoutPage = () => {
|
export const LogoutPage = () => {
|
||||||
const { auth, oauth, tailscale } = useUserContext();
|
const { provider, username, isLoggedIn, email, oauthName } = useUserContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const redirectTimer = useRef<number | null>(null);
|
const redirectTimer = useRef<number | null>(null);
|
||||||
@@ -49,82 +47,42 @@ export const LogoutPage = () => {
|
|||||||
};
|
};
|
||||||
}, [redirectTimer]);
|
}, [redirectTimer]);
|
||||||
|
|
||||||
if (!auth.authenticated) {
|
if (!isLoggedIn) {
|
||||||
return <Navigate to="/login" replace />;
|
return <Navigate to="/login" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oauth.active) {
|
|
||||||
return (
|
|
||||||
<LogoutLayout logoutMutation={logoutMutation}>
|
|
||||||
<Trans
|
|
||||||
i18nKey="logoutOauthSubtitle"
|
|
||||||
t={t}
|
|
||||||
components={{
|
|
||||||
code: <code />,
|
|
||||||
}}
|
|
||||||
values={{
|
|
||||||
username: auth.email,
|
|
||||||
provider: oauth.displayName,
|
|
||||||
}}
|
|
||||||
shouldUnescape={true}
|
|
||||||
/>
|
|
||||||
</LogoutLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auth.providerId === "tailscale") {
|
|
||||||
return (
|
|
||||||
<LogoutLayout logoutMutation={logoutMutation}>
|
|
||||||
<Trans
|
|
||||||
i18nKey="logoutTailscaleSubtitle"
|
|
||||||
t={t}
|
|
||||||
components={{
|
|
||||||
code: <code />,
|
|
||||||
}}
|
|
||||||
values={{
|
|
||||||
deviceName: tailscale.nodeName,
|
|
||||||
}}
|
|
||||||
shouldUnescape={true}
|
|
||||||
/>
|
|
||||||
</LogoutLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LogoutLayout logoutMutation={logoutMutation}>
|
|
||||||
<Trans
|
|
||||||
i18nKey="logoutUsernameSubtitle"
|
|
||||||
t={t}
|
|
||||||
components={{
|
|
||||||
code: <code />,
|
|
||||||
}}
|
|
||||||
values={{
|
|
||||||
username: auth.username,
|
|
||||||
}}
|
|
||||||
shouldUnescape={true}
|
|
||||||
/>
|
|
||||||
</LogoutLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface LogoutLayoutProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
logoutMutation: UseMutationResult<
|
|
||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-empty-object-type
|
|
||||||
AxiosResponse<any, any, {}>,
|
|
||||||
Error,
|
|
||||||
void,
|
|
||||||
unknown
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function LogoutLayout({ children, logoutMutation }: LogoutLayoutProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="gap-1.5">
|
<CardHeader className="gap-1.5">
|
||||||
<CardTitle className="text-xl">{t("logoutTitle")}</CardTitle>
|
<CardTitle className="text-xl">{t("logoutTitle")}</CardTitle>
|
||||||
<CardDescription>{children}</CardDescription>
|
<CardDescription>
|
||||||
|
{provider !== "local" && provider !== "ldap" ? (
|
||||||
|
<Trans
|
||||||
|
i18nKey="logoutOauthSubtitle"
|
||||||
|
t={t}
|
||||||
|
components={{
|
||||||
|
code: <code />,
|
||||||
|
}}
|
||||||
|
values={{
|
||||||
|
username: email,
|
||||||
|
provider: oauthName,
|
||||||
|
}}
|
||||||
|
shouldUnescape={true}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Trans
|
||||||
|
i18nKey="logoutUsernameSubtitle"
|
||||||
|
t={t}
|
||||||
|
components={{
|
||||||
|
code: <code />,
|
||||||
|
}}
|
||||||
|
values={{
|
||||||
|
username,
|
||||||
|
}}
|
||||||
|
shouldUnescape={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
<Button
|
<Button
|
||||||
@@ -138,4 +96,4 @@ function LogoutLayout({ children, logoutMutation }: LogoutLayoutProps) {
|
|||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { toast } from "sonner";
|
|||||||
import { useOIDCParams } from "@/lib/hooks/oidc";
|
import { useOIDCParams } from "@/lib/hooks/oidc";
|
||||||
|
|
||||||
export const TotpPage = () => {
|
export const TotpPage = () => {
|
||||||
const { totp } = useUserContext();
|
const { totpPending } = useUserContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const formId = useId();
|
const formId = useId();
|
||||||
@@ -64,7 +64,7 @@ export const TotpPage = () => {
|
|||||||
};
|
};
|
||||||
}, [redirectTimer]);
|
}, [redirectTimer]);
|
||||||
|
|
||||||
if (!totp.pending) {
|
if (!totpPending) {
|
||||||
return <Navigate to="/" replace />;
|
return <Navigate to="/" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,32 +6,15 @@ export const providerSchema = z.object({
|
|||||||
oauth: z.boolean(),
|
oauth: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const authSchema = z.object({
|
export const appContextSchema = z.object({
|
||||||
providers: z.array(providerSchema),
|
providers: z.array(providerSchema),
|
||||||
});
|
|
||||||
|
|
||||||
const oauthSchema = z.object({
|
|
||||||
autoRedirect: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const uiSchema = z.object({
|
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
|
appUrl: z.string(),
|
||||||
|
cookieDomain: z.string(),
|
||||||
forgotPasswordMessage: z.string(),
|
forgotPasswordMessage: z.string(),
|
||||||
backgroundImage: z.string(),
|
backgroundImage: z.string(),
|
||||||
|
oauthAutoRedirect: z.string(),
|
||||||
warningsEnabled: z.boolean(),
|
warningsEnabled: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const appSchema = z.object({
|
|
||||||
appUrl: z.string(),
|
|
||||||
cookieDomain: z.string(),
|
|
||||||
trustedDomains: z.array(z.string()),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const appContextSchema = z.object({
|
|
||||||
auth: authSchema,
|
|
||||||
oauth: oauthSchema,
|
|
||||||
ui: uiSchema,
|
|
||||||
app: appSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type AppContextSchema = z.infer<typeof appContextSchema>;
|
export type AppContextSchema = z.infer<typeof appContextSchema>;
|
||||||
|
|||||||
@@ -1,31 +1,14 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const authSchema = z.object({
|
export const userContextSchema = z.object({
|
||||||
authenticated: z.boolean(),
|
isLoggedIn: z.boolean(),
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
email: z.string(),
|
email: z.string(),
|
||||||
providerId: z.string(),
|
provider: z.string(),
|
||||||
});
|
oauth: z.boolean(),
|
||||||
|
totpPending: z.boolean(),
|
||||||
const oauthSchema = z.object({
|
oauthName: z.string(),
|
||||||
active: z.boolean(),
|
|
||||||
displayName: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const totpSchema = z.object({
|
|
||||||
pending: z.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const tailscaleSchema = z.object({
|
|
||||||
nodeName: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const userContextSchema = z.object({
|
|
||||||
auth: authSchema,
|
|
||||||
oauth: oauthSchema,
|
|
||||||
totp: totpSchema,
|
|
||||||
tailscale: tailscaleSchema,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UserContextSchema = z.infer<typeof userContextSchema>;
|
export type UserContextSchema = z.infer<typeof userContextSchema>;
|
||||||
|
|||||||
@@ -1,473 +0,0 @@
|
|||||||
// gen/sqlc-wrapper generates store.go wrapper files for each sqlc driver package under
|
|
||||||
// internal/repository/<driver>/. Run via:
|
|
||||||
//
|
|
||||||
// go generate ./internal/repository/...
|
|
||||||
//
|
|
||||||
// The generator introspects *Queries methods and the model/params types in the
|
|
||||||
// driver package, then emits a store.go that wraps *Queries so it satisfies
|
|
||||||
// repository.Store using the canonical shared types in the parent package.
|
|
||||||
// This generator is specific to sqlc-generated drivers. Non-sqlc drivers should
|
|
||||||
// implement repository.Store directly by hand.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
_ "embed"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"go/format"
|
|
||||||
"go/types"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"golang.org/x/tools/go/packages"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed store.tmpl
|
|
||||||
var storeSrc string
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("sqlc-wrapper: generating store.go files for sqlc driver packages...")
|
|
||||||
if err := run(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func run() error {
|
|
||||||
driverPkg := flag.String("pkg", "", "import path of the driver package")
|
|
||||||
out := flag.String("out", "store.go", "output filename relative to driver package directory")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if *driverPkg == "" {
|
|
||||||
return fmt.Errorf("-pkg is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the driver package directory so we can overlay the output file
|
|
||||||
// with a valid stub. This prevents a stale store.go from poisoning the
|
|
||||||
// type-checker and producing cryptic "undefined" errors.
|
|
||||||
driverDir, err := pkgDir(*driverPkg)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("resolve driver dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
outPath := filepath.Join(driverDir, *out)
|
|
||||||
if filepath.IsAbs(*out) {
|
|
||||||
outPath = *out
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stub replaces the output file during load so stale generated code is ignored.
|
|
||||||
stub := []byte("package " + filepath.Base(driverDir) + "\n")
|
|
||||||
cfg := &packages.Config{
|
|
||||||
Mode: packages.NeedName | packages.NeedTypes | packages.NeedSyntax | packages.NeedImports,
|
|
||||||
Overlay: map[string][]byte{outPath: stub},
|
|
||||||
}
|
|
||||||
|
|
||||||
driverTypePkg, err := loadOnePkg(cfg, *driverPkg)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("load driver package: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
repoPkgPath := parentPkg(*driverPkg)
|
|
||||||
repoTypePkg, err := loadOnePkg(cfg, repoPkgPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("load repo package: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateStructShapes(driverTypePkg, repoTypePkg); err != nil {
|
|
||||||
return fmt.Errorf("struct shape mismatch: %w", err)
|
|
||||||
}
|
|
||||||
if err := validateStoreCoverage(driverTypePkg, repoTypePkg); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
methods, err := collectMethods(driverTypePkg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
src, err := render(tmplData{
|
|
||||||
PkgName: driverTypePkg.Name(),
|
|
||||||
RepoPkg: repoPkgPath,
|
|
||||||
Methods: renderMethods(methods),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("render: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile(outPath, src, 0644); err != nil {
|
|
||||||
return fmt.Errorf("write %s: %w", outPath, err)
|
|
||||||
}
|
|
||||||
fmt.Printf("wrote %s\n", outPath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadOnePkg loads a single package via cfg and returns its *types.Package,
|
|
||||||
// or an error if the package fails to load or has type errors.
|
|
||||||
func loadOnePkg(cfg *packages.Config, importPath string) (*types.Package, error) {
|
|
||||||
pkgs, err := packages.Load(cfg, importPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("load %s: %w", importPath, err)
|
|
||||||
}
|
|
||||||
if len(pkgs) != 1 {
|
|
||||||
return nil, fmt.Errorf("expected 1 package for %s, got %d", importPath, len(pkgs))
|
|
||||||
}
|
|
||||||
pkg := pkgs[0]
|
|
||||||
if len(pkg.Errors) > 0 {
|
|
||||||
msgs := make([]string, len(pkg.Errors))
|
|
||||||
for i, e := range pkg.Errors {
|
|
||||||
msgs[i] = e.Error()
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("package %s has errors:\n %s", importPath, strings.Join(msgs, "\n "))
|
|
||||||
}
|
|
||||||
return pkg.Types, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parentPkg returns the parent import path (everything before the last /).
|
|
||||||
// Panics if imp contains no slash — callers are expected to pass driver sub-packages.
|
|
||||||
func parentPkg(imp string) string {
|
|
||||||
i := strings.LastIndex(imp, "/")
|
|
||||||
if i < 0 {
|
|
||||||
panic(fmt.Sprintf("parentPkg: import path %q has no parent", imp))
|
|
||||||
}
|
|
||||||
return imp[:i]
|
|
||||||
}
|
|
||||||
|
|
||||||
// pkgDir returns the on-disk directory for an import path using `go list`.
|
|
||||||
func pkgDir(importPath string) (string, error) {
|
|
||||||
out, err := exec.Command("go", "list", "-f", "{{.Dir}}", importPath).Output()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("go list %s: %w", importPath, err)
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(string(out)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// scopeStructs returns all named struct types in pkg, excluding the internal
|
|
||||||
// sqlc types Queries, DBTX, and Store. Names are returned in sorted order.
|
|
||||||
func scopeStructs(pkg *types.Package) (names []string, byName map[string]*types.Struct) {
|
|
||||||
byName = make(map[string]*types.Struct)
|
|
||||||
for _, name := range pkg.Scope().Names() { // Names() is already sorted
|
|
||||||
switch name {
|
|
||||||
case "Queries", "DBTX", "Store":
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
obj, ok := pkg.Scope().Lookup(name).(*types.TypeName)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
named, ok := obj.Type().(*types.Named)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s, ok := named.Underlying().(*types.Struct)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
names = append(names, name)
|
|
||||||
byName[name] = s
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateStoreCoverage checks that every method declared in repository.Store
|
|
||||||
// exists on *Queries in the driver package. Missing methods are reported by
|
|
||||||
// name so the developer knows exactly which SQL queries need to be added.
|
|
||||||
func validateStoreCoverage(driverPkg, repoPkg *types.Package) error {
|
|
||||||
queriesObj := driverPkg.Scope().Lookup("Queries")
|
|
||||||
if queriesObj == nil {
|
|
||||||
return fmt.Errorf("queries type not found in driver package")
|
|
||||||
}
|
|
||||||
queriesNamed := queriesObj.Type().(*types.Named)
|
|
||||||
queriesMS := types.NewMethodSet(types.NewPointer(queriesNamed))
|
|
||||||
queriesMethods := make(map[string]bool)
|
|
||||||
for m := range queriesMS.Methods() {
|
|
||||||
queriesMethods[m.Obj().Name()] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
storeObj := repoPkg.Scope().Lookup("Store")
|
|
||||||
if storeObj == nil {
|
|
||||||
return fmt.Errorf("store type not found in repository package")
|
|
||||||
}
|
|
||||||
storeIface, ok := storeObj.Type().Underlying().(*types.Interface)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("repository.Store is not an interface")
|
|
||||||
}
|
|
||||||
|
|
||||||
var missing []string
|
|
||||||
for method := range storeIface.Methods() {
|
|
||||||
if name := method.Name(); !queriesMethods[name] {
|
|
||||||
missing = append(missing, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(missing) > 0 {
|
|
||||||
sort.Strings(missing)
|
|
||||||
return fmt.Errorf(
|
|
||||||
"driver *Queries is missing %d method(s) required by repository.Store:\n - %s\n\nRun sqlc generate to regenerate query methods, or add the missing SQL queries",
|
|
||||||
len(missing), strings.Join(missing, "\n - "),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateStructShapes checks that every model/params struct in the driver
|
|
||||||
// package has fields that exactly match the corresponding type in the repo
|
|
||||||
// (parent) package. This catches drift between sqlc-generated types and the
|
|
||||||
// canonical repository types before a broken cast reaches the compiler.
|
|
||||||
func validateStructShapes(driverPkg, repoPkg *types.Package) error {
|
|
||||||
_, repoStructs := scopeStructs(repoPkg)
|
|
||||||
driverNames, driverStructs := scopeStructs(driverPkg)
|
|
||||||
|
|
||||||
var errs []string
|
|
||||||
for _, name := range driverNames {
|
|
||||||
repoStruct, ok := repoStructs[name]
|
|
||||||
if !ok {
|
|
||||||
// Driver has a type not in repo — fine (e.g. internal helpers).
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := compareStructs(name, driverStructs[name], repoStruct); err != nil {
|
|
||||||
errs = append(errs, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(errs) > 0 {
|
|
||||||
sort.Strings(errs)
|
|
||||||
return fmt.Errorf("%s", strings.Join(errs, "\n "))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func compareStructs(name string, driver, repo *types.Struct) error {
|
|
||||||
if driver.NumFields() != repo.NumFields() {
|
|
||||||
return fmt.Errorf("%s: field count mismatch (driver=%d, repo=%d)",
|
|
||||||
name, driver.NumFields(), repo.NumFields())
|
|
||||||
}
|
|
||||||
for i := range driver.NumFields() {
|
|
||||||
df := driver.Field(i)
|
|
||||||
rf := repo.Field(i)
|
|
||||||
if df.Name() != rf.Name() {
|
|
||||||
return fmt.Errorf("%s: field %d name mismatch (driver=%q, repo=%q)",
|
|
||||||
name, i, df.Name(), rf.Name())
|
|
||||||
}
|
|
||||||
if !types.Identical(df.Type(), rf.Type()) {
|
|
||||||
return fmt.Errorf("%s.%s: type mismatch (driver=%s, repo=%s)",
|
|
||||||
name, df.Name(), df.Type(), rf.Type())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type methodInfo struct {
|
|
||||||
Name string
|
|
||||||
Params []paramInfo
|
|
||||||
Results []resultInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
type paramInfo struct {
|
|
||||||
Name string
|
|
||||||
TypeStr string // local (unqualified) type name
|
|
||||||
RepoType string // "repository.X" if this is a driver model/params type; else ""
|
|
||||||
}
|
|
||||||
|
|
||||||
type resultInfo struct {
|
|
||||||
TypeStr string
|
|
||||||
IsSlice bool
|
|
||||||
RepoType string // "repository.X" if driver type; else ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectMethods(pkg *types.Package) ([]methodInfo, error) {
|
|
||||||
obj := pkg.Scope().Lookup("Queries")
|
|
||||||
if obj == nil {
|
|
||||||
return nil, fmt.Errorf("queries type not found in %s", pkg.Path())
|
|
||||||
}
|
|
||||||
named, ok := obj.Type().(*types.Named)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("queries is not a named type")
|
|
||||||
}
|
|
||||||
ms := types.NewMethodSet(types.NewPointer(named))
|
|
||||||
|
|
||||||
var out []methodInfo
|
|
||||||
for method := range ms.Methods() {
|
|
||||||
fn, ok := method.Obj().(*types.Func)
|
|
||||||
if !ok || fn.Name() == "WithTx" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sig := fn.Type().(*types.Signature)
|
|
||||||
mi := methodInfo{Name: fn.Name()}
|
|
||||||
|
|
||||||
// params: skip receiver + first (context.Context)
|
|
||||||
for i := 1; i < sig.Params().Len(); i++ {
|
|
||||||
p := sig.Params().At(i)
|
|
||||||
mi.Params = append(mi.Params, makeParam(p.Name(), p.Type(), pkg.Path()))
|
|
||||||
}
|
|
||||||
// results: skip error
|
|
||||||
for r := range sig.Results().Variables() {
|
|
||||||
if r.Type().String() == "error" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mi.Results = append(mi.Results, makeResult(r.Type(), pkg.Path()))
|
|
||||||
}
|
|
||||||
out = append(out, mi)
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeParam(name string, t types.Type, driverPath string) paramInfo {
|
|
||||||
return paramInfo{
|
|
||||||
Name: name,
|
|
||||||
TypeStr: localName(t, driverPath),
|
|
||||||
RepoType: repoName(t, driverPath),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeResult(t types.Type, driverPath string) resultInfo {
|
|
||||||
ri := resultInfo{}
|
|
||||||
if sl, ok := t.(*types.Slice); ok {
|
|
||||||
ri.IsSlice = true
|
|
||||||
t = sl.Elem()
|
|
||||||
}
|
|
||||||
ri.TypeStr = localName(t, driverPath)
|
|
||||||
ri.RepoType = repoName(t, driverPath)
|
|
||||||
return ri
|
|
||||||
}
|
|
||||||
|
|
||||||
func localName(t types.Type, driverPath string) string {
|
|
||||||
named, ok := t.(*types.Named)
|
|
||||||
if !ok {
|
|
||||||
return types.TypeString(t, nil)
|
|
||||||
}
|
|
||||||
if named.Obj().Pkg() != nil && named.Obj().Pkg().Path() == driverPath {
|
|
||||||
return named.Obj().Name()
|
|
||||||
}
|
|
||||||
return types.TypeString(t, func(p *types.Package) string { return p.Name() })
|
|
||||||
}
|
|
||||||
|
|
||||||
func repoName(t types.Type, driverPath string) string {
|
|
||||||
named, ok := t.(*types.Named)
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if named.Obj().Pkg() != nil && named.Obj().Pkg().Path() == driverPath {
|
|
||||||
return "repository." + named.Obj().Name()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderedMethod holds pre-built signature and body strings passed to the template.
|
|
||||||
type renderedMethod struct {
|
|
||||||
Signature string
|
|
||||||
Body string
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderMethods(methods []methodInfo) []renderedMethod {
|
|
||||||
out := make([]renderedMethod, len(methods))
|
|
||||||
for i, m := range methods {
|
|
||||||
out[i] = renderedMethod{
|
|
||||||
Signature: buildSig(m),
|
|
||||||
Body: buildBody(m),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildSig(m methodInfo) string {
|
|
||||||
var sb strings.Builder
|
|
||||||
sb.WriteString("func (s *Store) ")
|
|
||||||
sb.WriteString(m.Name)
|
|
||||||
sb.WriteString("(ctx context.Context")
|
|
||||||
for _, p := range m.Params {
|
|
||||||
sb.WriteString(", ")
|
|
||||||
sb.WriteString(p.Name)
|
|
||||||
sb.WriteString(" ")
|
|
||||||
if p.RepoType != "" {
|
|
||||||
sb.WriteString(p.RepoType)
|
|
||||||
} else {
|
|
||||||
sb.WriteString(p.TypeStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sb.WriteString(") (")
|
|
||||||
for _, r := range m.Results {
|
|
||||||
if r.IsSlice {
|
|
||||||
sb.WriteString("[]")
|
|
||||||
}
|
|
||||||
if r.RepoType != "" {
|
|
||||||
sb.WriteString(r.RepoType)
|
|
||||||
} else {
|
|
||||||
sb.WriteString(r.TypeStr)
|
|
||||||
}
|
|
||||||
sb.WriteString(", ")
|
|
||||||
}
|
|
||||||
sb.WriteString("error)")
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func callArgs(m methodInfo) string {
|
|
||||||
args := make([]string, 0, len(m.Params))
|
|
||||||
for _, p := range m.Params {
|
|
||||||
if p.RepoType != "" {
|
|
||||||
// convert repo type → driver type: DriverType(arg)
|
|
||||||
args = append(args, p.TypeStr+"("+p.Name+")")
|
|
||||||
} else {
|
|
||||||
args = append(args, p.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(args) == 0 {
|
|
||||||
return "ctx"
|
|
||||||
}
|
|
||||||
return "ctx, " + strings.Join(args, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
var bodyTmpl = template.Must(template.New("store").Parse(storeSrc))
|
|
||||||
|
|
||||||
type bodyData struct {
|
|
||||||
Call string
|
|
||||||
RepoType string
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildBody(m methodInfo) string {
|
|
||||||
call := "s.q." + m.Name + "(" + callArgs(m) + ")"
|
|
||||||
|
|
||||||
var (
|
|
||||||
name string
|
|
||||||
data bodyData
|
|
||||||
)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case len(m.Results) == 0 || m.Results[0].RepoType == "":
|
|
||||||
name = "void"
|
|
||||||
data = bodyData{Call: call}
|
|
||||||
case m.Results[0].IsSlice:
|
|
||||||
name = "slice"
|
|
||||||
data = bodyData{Call: call, RepoType: m.Results[0].RepoType}
|
|
||||||
default:
|
|
||||||
name = "scalar"
|
|
||||||
data = bodyData{Call: call, RepoType: m.Results[0].RepoType}
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := bodyTmpl.ExecuteTemplate(&buf, name, data); err != nil {
|
|
||||||
panic(fmt.Sprintf("buildBody %s: %v", name, err))
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type tmplData struct {
|
|
||||||
PkgName string
|
|
||||||
RepoPkg string
|
|
||||||
Methods []renderedMethod
|
|
||||||
}
|
|
||||||
|
|
||||||
func render(data tmplData) ([]byte, error) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := bodyTmpl.Execute(&buf, data); err != nil {
|
|
||||||
return nil, fmt.Errorf("execute template: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
formatted, err := format.Source(buf.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return buf.Bytes(), fmt.Errorf("format source: %w\nraw:\n%s", err, buf.String())
|
|
||||||
}
|
|
||||||
return formatted, nil
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
// Code generated by cmd/gen/sqlc-wrapper. DO NOT EDIT.
|
|
||||||
package {{.PkgName}}
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"{{.RepoPkg}}"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Store wraps *Queries and implements repository.Store.
|
|
||||||
type Store struct {
|
|
||||||
q *Queries
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStore wraps a *Queries to satisfy repository.Store.
|
|
||||||
func NewStore(q *Queries) repository.Store {
|
|
||||||
return &Store{q: q}
|
|
||||||
}
|
|
||||||
|
|
||||||
var errorMap = map[error]error{
|
|
||||||
sql.ErrNoRows: repository.ErrNotFound,
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapErr(err error) error {
|
|
||||||
for from, to := range errorMap {
|
|
||||||
if errors.Is(err, from) {
|
|
||||||
return to
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
{{range .Methods}}{{.Signature}} {
|
|
||||||
{{.Body}}}
|
|
||||||
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{- define "void"}} return mapErr({{.Call}})
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{- define "scalar"}} r, err := {{.Call}}
|
|
||||||
if err != nil {
|
|
||||||
return {{.RepoType}}{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return {{.RepoType}}(r), nil
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{- define "slice"}} rows, err := {{.Call}}
|
|
||||||
if err != nil {
|
|
||||||
return nil, mapErr(err)
|
|
||||||
}
|
|
||||||
out := make([]{{.RepoType}}, len(rows))
|
|
||||||
for i, row := range rows {
|
|
||||||
out[i] = {{.RepoType}}(row)
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
{{end}}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
module github.com/tinyauthapp/tinyauth
|
module github.com/tinyauthapp/tinyauth
|
||||||
|
|
||||||
go 1.26.1
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
charm.land/huh/v2 v2.0.3
|
charm.land/huh/v2 v2.0.3
|
||||||
@@ -20,11 +20,9 @@ require (
|
|||||||
github.com/weppos/publicsuffix-go v0.50.3
|
github.com/weppos/publicsuffix-go v0.50.3
|
||||||
golang.org/x/crypto v0.50.0
|
golang.org/x/crypto v0.50.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/tools v0.43.0
|
|
||||||
k8s.io/apimachinery v0.36.0
|
k8s.io/apimachinery v0.36.0
|
||||||
k8s.io/client-go v0.36.0
|
k8s.io/client-go v0.36.0
|
||||||
modernc.org/sqlite v1.50.0
|
modernc.org/sqlite v1.50.0
|
||||||
tailscale.com v1.96.5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -32,29 +30,13 @@ require (
|
|||||||
charm.land/bubbletea/v2 v2.0.2 // indirect
|
charm.land/bubbletea/v2 v2.0.2 // indirect
|
||||||
charm.land/lipgloss/v2 v2.0.1 // indirect
|
charm.land/lipgloss/v2 v2.0.1 // indirect
|
||||||
dario.cat/mergo v1.0.1 // indirect
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
filippo.io/edwards25519 v1.2.0 // indirect
|
|
||||||
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
||||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/akutz/memconn v0.1.0 // indirect
|
|
||||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e // indirect
|
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.29.5 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
|
|
||||||
github.com/aws/smithy-go v1.24.0 // indirect
|
|
||||||
github.com/boombuler/barcode v1.0.2 // indirect
|
github.com/boombuler/barcode v1.0.2 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic v1.15.0 // indirect
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
@@ -72,12 +54,10 @@ require (
|
|||||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/coder/websocket v1.8.12 // indirect
|
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
github.com/creachadair/msync v0.7.1 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/go-connections v0.5.0 // indirect
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
@@ -85,10 +65,8 @@ require (
|
|||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
github.com/gaissmai/bart v0.26.1 // indirect
|
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
|
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
@@ -96,16 +74,8 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
|
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
|
||||||
github.com/google/btree v1.1.3 // indirect
|
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/huin/goupnp v1.3.0 // indirect
|
|
||||||
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.18.2 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
@@ -113,46 +83,35 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
|
||||||
github.com/mdlayher/socket v0.5.0 // indirect
|
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
|
github.com/moby/term v0.5.2 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus-community/pro-bing v0.4.0 // indirect
|
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/safchain/ethtool v0.3.0 // indirect
|
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
|
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
|
||||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
|
|
||||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
|
|
||||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
|
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||||
@@ -160,24 +119,18 @@ require (
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
|
||||||
golang.org/x/arch v0.22.0 // indirect
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
golang.org/x/mod v0.34.0 // indirect
|
|
||||||
golang.org/x/net v0.52.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
golang.org/x/term v0.42.0 // indirect
|
golang.org/x/term v0.42.0 // indirect
|
||||||
golang.org/x/text v0.36.0 // indirect
|
golang.org/x/text v0.36.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gotest.tools/v3 v3.5.2 // indirect
|
gotest.tools/v3 v3.5.2 // indirect
|
||||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect
|
|
||||||
k8s.io/klog/v2 v2.140.0 // indirect
|
k8s.io/klog/v2 v2.140.0 // indirect
|
||||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
||||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q=
|
|
||||||
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM=
|
|
||||||
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
||||||
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
||||||
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
||||||
@@ -10,10 +8,6 @@ charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
|
|||||||
charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
||||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
|
||||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
|
||||||
filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
|
|
||||||
filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
||||||
@@ -24,50 +18,16 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
|
|||||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
|
||||||
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
|
||||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=
|
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg=
|
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=
|
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A=
|
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8=
|
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
|
|
||||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
|
||||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
|
||||||
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02 h1:bXAPYSbdYbS5VTy92NIUbeDI1qyggi+JYh5op9IFlcQ=
|
|
||||||
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c=
|
|
||||||
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||||
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
@@ -109,46 +69,26 @@ github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2
|
|||||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||||
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
|
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
|
||||||
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
|
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
|
||||||
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
|
|
||||||
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
|
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
|
||||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
|
||||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
|
|
||||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
|
||||||
github.com/creachadair/mds v0.25.9 h1:080Hr8laN2h+l3NeVCGMBpXtIPnl9mz8e4HLraGPqtA=
|
|
||||||
github.com/creachadair/mds v0.25.9/go.mod h1:4hatI3hRM+qhzuAmqPRFvaBM8mONkS7nsLxkcuTYUIs=
|
|
||||||
github.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7uho=
|
|
||||||
github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008=
|
|
||||||
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
|
||||||
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
|
||||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/davecgh/go-spew v1.1.0/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
|
|
||||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
|
|
||||||
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8=
|
|
||||||
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
|
||||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
|
||||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
|
||||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
|
||||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
@@ -167,20 +107,14 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
|
|||||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
|
|
||||||
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
|
||||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I=
|
|
||||||
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
|
|
||||||
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
@@ -188,12 +122,10 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
|||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||||
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
|
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||||
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
|
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
@@ -204,20 +136,12 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo=
|
|
||||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g=
|
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
|
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
|
||||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
|
||||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
|
||||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
@@ -225,11 +149,7 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||||
github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I=
|
|
||||||
github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
|
|
||||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -238,19 +158,10 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz
|
|||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
|
||||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
|
||||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
|
||||||
github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=
|
|
||||||
github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=
|
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
|
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
|
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||||
@@ -263,24 +174,12 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
|
|||||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||||
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
|
|
||||||
github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
|
|
||||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
|
||||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
|
|
||||||
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
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/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
|
||||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
|
||||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
|
||||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -301,22 +200,10 @@ github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjc
|
|||||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
|
||||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
|
||||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
|
||||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
|
|
||||||
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
|
|
||||||
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
|
||||||
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
|
||||||
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
|
|
||||||
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
||||||
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
||||||
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
|
||||||
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
|
||||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
|
||||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||||
@@ -343,33 +230,19 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
|
|
||||||
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
|
||||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
|
||||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
|
||||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
|
|
||||||
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
|
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
|
||||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
|
||||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
|
||||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
@@ -382,8 +255,6 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
|
|||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||||
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
|
|
||||||
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
|
|
||||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
@@ -404,40 +275,12 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
|
|
||||||
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
|
||||||
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM=
|
|
||||||
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
|
|
||||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
|
|
||||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
|
|
||||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
|
|
||||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
|
||||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
|
|
||||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
|
|
||||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
|
|
||||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
|
||||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
|
|
||||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
|
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da h1:jVRUZPRs9sqyKlYHHzHjAqKN+6e/Vog6NpHYeNPJqOw=
|
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
|
||||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
|
|
||||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
|
||||||
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
|
||||||
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
|
||||||
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 h1:EYSb5jv8ZL/0/NVFZtY7Ejplk0QG5+3lrdL3mSrjFZQ=
|
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 h1:EYSb5jv8ZL/0/NVFZtY7Ejplk0QG5+3lrdL3mSrjFZQ=
|
||||||
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298/go.mod h1:TlUDoCF66hMqFZqoBym9bUdJ0bKAWYMir6hLJeYN5z0=
|
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298/go.mod h1:TlUDoCF66hMqFZqoBym9bUdJ0bKAWYMir6hLJeYN5z0=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg=
|
|
||||||
github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE=
|
|
||||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
|
||||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
|
||||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
|
||||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
|
||||||
github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE=
|
github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE=
|
||||||
github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
|
github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
@@ -448,8 +291,8 @@ go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF
|
|||||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||||
@@ -472,30 +315,20 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
|||||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
|
||||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=
|
|
||||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
|
||||||
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
|
|
||||||
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
|
|
||||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
@@ -507,10 +340,6 @@ golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
|||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
|
||||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||||
@@ -532,12 +361,6 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 h1:Zy8IV/+FMLxy6j6p87vk/vQGKcdnbprwjTxc8UiUtsA=
|
|
||||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
|
|
||||||
honnef.co/go/tools v0.7.0-0.dev.0.20251022135355-8273271481d0 h1:5SXjd4ET5dYijLaf0O3aOenC0Z4ZafIWSpjUzsQaNho=
|
|
||||||
honnef.co/go/tools v0.7.0-0.dev.0.20251022135355-8273271481d0/go.mod h1:EPDDhEZqVHhWuPI5zPAsjU0U7v9xNIWjoOVyZ5ZcniQ=
|
|
||||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
|
||||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
|
||||||
k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80=
|
k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80=
|
||||||
k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34=
|
k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34=
|
||||||
k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
|
k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
|
||||||
@@ -588,7 +411,3 @@ sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80
|
|||||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||||
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
|
|
||||||
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
|
||||||
tailscale.com v1.96.5 h1:gNkfA/KSZAl6jCH9cj8urq00HRWItDDTtGsyATI89jA=
|
|
||||||
tailscale.com v1.96.5/go.mod h1:/3lnZBYb2UEwnN0MNu2SDXUtT06AGd5k0s+OWx3WmcY=
|
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ var FrontendAssets embed.FS
|
|||||||
|
|
||||||
// Migrations
|
// Migrations
|
||||||
//
|
//
|
||||||
//go:embed migrations/sqlite/*.sql
|
//go:embed migrations/*.sql
|
||||||
var Migrations embed.FS
|
var Migrations embed.FS
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -18,7 +19,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
@@ -34,22 +34,19 @@ type Services struct {
|
|||||||
ldapService *service.LdapService
|
ldapService *service.LdapService
|
||||||
oauthBrokerService *service.OAuthBrokerService
|
oauthBrokerService *service.OAuthBrokerService
|
||||||
oidcService *service.OIDCService
|
oidcService *service.OIDCService
|
||||||
tailscaleService *service.TailscaleService
|
|
||||||
policyEngine *service.PolicyEngine
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BootstrapApp struct {
|
type BootstrapApp struct {
|
||||||
config model.Config
|
config model.Config
|
||||||
runtime model.RuntimeConfig
|
runtime model.RuntimeConfig
|
||||||
services Services
|
services Services
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
queries repository.Store
|
queries *repository.Queries
|
||||||
router *gin.Engine
|
router *gin.Engine
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
listeners []Listener
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBootstrapApp(config model.Config) *BootstrapApp {
|
func NewBootstrapApp(config model.Config) *BootstrapApp {
|
||||||
@@ -69,8 +66,6 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
log.Init()
|
log.Init()
|
||||||
app.log = log
|
app.log = log
|
||||||
|
|
||||||
app.log.App.Info().Msgf("Starting Tinyauth version: %s", model.Version)
|
|
||||||
|
|
||||||
// get app url
|
// get app url
|
||||||
if app.config.AppURL == "" {
|
if app.config.AppURL == "" {
|
||||||
return errors.New("app url cannot be empty, perhaps config loading failed")
|
return errors.New("app url cannot be empty, perhaps config loading failed")
|
||||||
@@ -83,7 +78,6 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.runtime.AppURL = appUrl.Scheme + "://" + appUrl.Host
|
app.runtime.AppURL = appUrl.Scheme + "://" + appUrl.Host
|
||||||
app.runtime.TrustedDomains = append(app.runtime.TrustedDomains, app.runtime.AppURL)
|
|
||||||
|
|
||||||
// validate session config
|
// validate session config
|
||||||
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
|
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
|
||||||
@@ -168,7 +162,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
app.runtime.OAuthSessionCookieName = fmt.Sprintf("%s-%s", model.OAuthSessionCookieName, cookieId)
|
app.runtime.OAuthSessionCookieName = fmt.Sprintf("%s-%s", model.OAuthSessionCookieName, cookieId)
|
||||||
|
|
||||||
// database
|
// database
|
||||||
store, err := app.SetupStore()
|
err = app.SetupDatabase()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to setup database: %w", err)
|
return fmt.Errorf("failed to setup database: %w", err)
|
||||||
@@ -179,13 +173,12 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
defer func() {
|
defer func() {
|
||||||
app.cancel()
|
app.cancel()
|
||||||
app.wg.Wait()
|
app.wg.Wait()
|
||||||
if app.db != nil {
|
app.db.Close()
|
||||||
app.db.Close()
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// store
|
// queries
|
||||||
app.queries = store
|
queries := repository.New(app.db)
|
||||||
|
app.queries = queries
|
||||||
|
|
||||||
// services
|
// services
|
||||||
err = app.setupServices()
|
err = app.setupServices()
|
||||||
@@ -235,11 +228,6 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
|
|
||||||
app.runtime.ConfiguredProviders = configuredProviders
|
app.runtime.ConfiguredProviders = configuredProviders
|
||||||
|
|
||||||
// throw in tailscale if it's configured just before setting up the controllers
|
|
||||||
if app.services.tailscaleService != nil {
|
|
||||||
app.runtime.TrustedDomains = append(app.runtime.TrustedDomains, "https://"+app.services.tailscaleService.GetHostname())
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup router
|
// setup router
|
||||||
err = app.setupRouter()
|
err = app.setupRouter()
|
||||||
|
|
||||||
@@ -257,18 +245,42 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
app.wg.Go(app.heartbeatRoutine)
|
app.wg.Go(app.heartbeatRoutine)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup listeners
|
// create err channel to listen for server errors
|
||||||
app.listeners = app.calculateListenerPolicy()
|
errChanLen := 0
|
||||||
|
|
||||||
|
runUnix := app.config.Server.SocketPath != ""
|
||||||
|
runHTTP := app.config.Server.SocketPath == "" || app.config.Server.ConcurrentListenersEnabled
|
||||||
|
|
||||||
|
if runUnix {
|
||||||
|
errChanLen++
|
||||||
|
}
|
||||||
|
|
||||||
|
if runHTTP {
|
||||||
|
errChanLen++
|
||||||
|
}
|
||||||
|
|
||||||
|
errChan := make(chan error, errChanLen)
|
||||||
|
|
||||||
if app.config.Server.ConcurrentListenersEnabled {
|
if app.config.Server.ConcurrentListenersEnabled {
|
||||||
app.log.App.Info().Msg("Concurrent listeners enabled, will run on all available listeners")
|
app.log.App.Info().Msg("Concurrent listeners enabled, will run on all available listeners")
|
||||||
}
|
}
|
||||||
|
|
||||||
// run listeners
|
// serve unix
|
||||||
lec, err := app.runListeners()
|
if runUnix {
|
||||||
|
app.wg.Go(func() {
|
||||||
|
if err := app.serveUnix(); err != nil {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
// serve to http
|
||||||
return fmt.Errorf("failed to run listeners: %w", err)
|
if runHTTP {
|
||||||
|
app.wg.Go(func() {
|
||||||
|
if err := app.serveHTTP(); err != nil {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// monitor cancellation and server errors
|
// monitor cancellation and server errors
|
||||||
@@ -277,14 +289,89 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
case <-app.ctx.Done():
|
case <-app.ctx.Done():
|
||||||
app.log.App.Info().Msg("Oh, it's time for me to go, bye!")
|
app.log.App.Info().Msg("Oh, it's time for me to go, bye!")
|
||||||
return nil
|
return nil
|
||||||
case err := <-lec:
|
case err := <-errChan:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("listener error: %w", err)
|
return fmt.Errorf("server error: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *BootstrapApp) serveHTTP() error {
|
||||||
|
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
|
||||||
|
|
||||||
|
app.log.App.Info().Msgf("Starting server on %s", address)
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: address,
|
||||||
|
Handler: app.router.Handler(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-app.ctx.Done()
|
||||||
|
app.log.App.Debug().Msg("Shutting down http listener")
|
||||||
|
server.Shutdown(app.ctx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := server.ListenAndServe()
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
return fmt.Errorf("failed to start http listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *BootstrapApp) serveUnix() error {
|
||||||
|
if app.config.Server.SocketPath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := os.Stat(app.config.Server.SocketPath)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
app.log.App.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath)
|
||||||
|
err := os.Remove(app.config.Server.SocketPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to remove existing socket file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.log.App.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath)
|
||||||
|
|
||||||
|
listener, err := net.Listen("unix", app.config.Server.SocketPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create unix socket listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Handler: app.router.Handler(),
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown := func() {
|
||||||
|
server.Shutdown(app.ctx)
|
||||||
|
listener.Close()
|
||||||
|
os.Remove(app.config.Server.SocketPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-app.ctx.Done()
|
||||||
|
app.log.App.Debug().Msg("Shutting down unix socket listener")
|
||||||
|
shutdown()
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = server.Serve(listener)
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
shutdown()
|
||||||
|
return fmt.Errorf("failed to start unix socket listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (app *BootstrapApp) heartbeatRoutine() {
|
func (app *BootstrapApp) heartbeatRoutine() {
|
||||||
ticker := time.NewTicker(time.Duration(12) * time.Hour)
|
ticker := time.NewTicker(time.Duration(12) * time.Hour)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/assets"
|
"github.com/tinyauthapp/tinyauth/internal/assets"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository/sqlite"
|
|
||||||
|
|
||||||
"github.com/golang-migrate/migrate/v4"
|
"github.com/golang-migrate/migrate/v4"
|
||||||
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||||
@@ -17,28 +14,17 @@ import (
|
|||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (app *BootstrapApp) SetupStore() (repository.Store, error) {
|
func (app *BootstrapApp) SetupDatabase() error {
|
||||||
switch app.config.Database.Driver {
|
dir := filepath.Dir(app.config.Database.Path)
|
||||||
case "memory":
|
|
||||||
return memory.New(), nil
|
|
||||||
case "sqlite", "":
|
|
||||||
return app.setupSQLite(app.config.Database.Path)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown database driver %q: valid values are sqlite, memory", app.config.Database.Driver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *BootstrapApp) setupSQLite(databasePath string) (repository.Store, error) {
|
|
||||||
dir := filepath.Dir(databasePath)
|
|
||||||
|
|
||||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||||
return nil, fmt.Errorf("failed to create database directory %s: %w", dir, err)
|
return fmt.Errorf("failed to create database directory %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := sql.Open("sqlite", databasePath)
|
db, err := sql.Open("sqlite", app.config.Database.Path)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
return fmt.Errorf("failed to open database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the database if there is an error during migration
|
// Close the database if there is an error during migration
|
||||||
@@ -52,29 +38,32 @@ func (app *BootstrapApp) setupSQLite(databasePath string) (repository.Store, err
|
|||||||
// if the sqlite connection starts being a bottleneck
|
// if the sqlite connection starts being a bottleneck
|
||||||
db.SetMaxOpenConns(1)
|
db.SetMaxOpenConns(1)
|
||||||
|
|
||||||
migrations, err := iofs.New(assets.Migrations, "migrations/sqlite")
|
migrations, err := iofs.New(assets.Migrations, "migrations")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create migrations: %w", err)
|
return fmt.Errorf("failed to create migrations: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
target, err := sqlite3.WithInstance(db, &sqlite3.Config{})
|
target, err := sqlite3.WithInstance(db, &sqlite3.Config{})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create sqlite3 instance: %w", err)
|
return fmt.Errorf("failed to create sqlite3 instance: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
migrator, err := migrate.NewWithInstance("iofs", migrations, "sqlite3", target)
|
migrator, err := migrate.NewWithInstance("iofs", migrations, "sqlite3", target)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create migrator: %w", err)
|
return fmt.Errorf("failed to create migrator: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := migrator.Up(); err != nil && err != migrate.ErrNoChange {
|
if err := migrator.Up(); err != nil && err != migrate.ErrNoChange {
|
||||||
return nil, fmt.Errorf("failed to migrate database: %w", err)
|
return fmt.Errorf("failed to migrate database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.db = db
|
app.db = db
|
||||||
|
return nil
|
||||||
return sqlite.NewStore(sqlite.New(db)), nil
|
}
|
||||||
|
|
||||||
|
func (app *BootstrapApp) GetDB() *sql.DB {
|
||||||
|
return app.db
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,14 @@
|
|||||||
package bootstrap
|
package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/middleware"
|
"github.com/tinyauthapp/tinyauth/internal/middleware"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Listener int
|
|
||||||
|
|
||||||
const (
|
|
||||||
ListenerHTTP Listener = iota
|
|
||||||
ListenerUnix
|
|
||||||
ListenerTailscale
|
|
||||||
)
|
|
||||||
|
|
||||||
func (app *BootstrapApp) setupRouter() error {
|
func (app *BootstrapApp) setupRouter() error {
|
||||||
// we don't want gin debug mode
|
// we don't want gin debug mode
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
@@ -39,7 +24,7 @@ func (app *BootstrapApp) setupRouter() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contextMiddleware := middleware.NewContextMiddleware(app.log, app.runtime, app.services.authService, app.services.oauthBrokerService, app.services.tailscaleService)
|
contextMiddleware := middleware.NewContextMiddleware(app.log, app.runtime, app.services.authService, app.services.oauthBrokerService)
|
||||||
engine.Use(contextMiddleware.Middleware())
|
engine.Use(contextMiddleware.Middleware())
|
||||||
|
|
||||||
uiMiddleware, err := middleware.NewUIMiddleware()
|
uiMiddleware, err := middleware.NewUIMiddleware()
|
||||||
@@ -59,7 +44,7 @@ func (app *BootstrapApp) setupRouter() error {
|
|||||||
controller.NewContextController(app.log, app.config, app.runtime, apiRouter)
|
controller.NewContextController(app.log, app.config, app.runtime, apiRouter)
|
||||||
controller.NewOAuthController(app.log, app.config, app.runtime, apiRouter, app.services.authService)
|
controller.NewOAuthController(app.log, app.config, app.runtime, apiRouter, app.services.authService)
|
||||||
controller.NewOIDCController(app.log, app.services.oidcService, app.runtime, apiRouter)
|
controller.NewOIDCController(app.log, app.services.oidcService, app.runtime, apiRouter)
|
||||||
controller.NewProxyController(app.log, app.runtime, apiRouter, app.services.accessControlService, app.services.authService, app.services.policyEngine)
|
controller.NewProxyController(app.log, app.runtime, apiRouter, app.services.accessControlService, app.services.authService)
|
||||||
controller.NewUserController(app.log, app.runtime, apiRouter, app.services.authService)
|
controller.NewUserController(app.log, app.runtime, apiRouter, app.services.authService)
|
||||||
controller.NewResourcesController(app.config, &engine.RouterGroup)
|
controller.NewResourcesController(app.config, &engine.RouterGroup)
|
||||||
controller.NewHealthController(apiRouter)
|
controller.NewHealthController(apiRouter)
|
||||||
@@ -68,161 +53,3 @@ func (app *BootstrapApp) setupRouter() error {
|
|||||||
app.router = engine
|
app.router = engine
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *BootstrapApp) runListeners() (chan error, error) {
|
|
||||||
// lec -> listener error channel
|
|
||||||
lec := make(chan error, len(app.listeners))
|
|
||||||
|
|
||||||
for _, listenerType := range app.listeners {
|
|
||||||
listenerFunc, err := app.listenerFromType(listenerType)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get listener function: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.wg.Go(func() {
|
|
||||||
lec <- listenerFunc()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return lec, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// The way we calculate listeners is as follows:
|
|
||||||
// If concurrent listeners are disabled, we pick the first available listener, so:
|
|
||||||
// 1. If tailscale is enabled, we use tailscale
|
|
||||||
// 2. If socket path is configured, we use unix socket
|
|
||||||
// 3. Finally if none is configured we use http
|
|
||||||
// If concurrent listeners are enabled, we add all available listeners in the following order
|
|
||||||
func (app *BootstrapApp) calculateListenerPolicy() []Listener {
|
|
||||||
l := []Listener{}
|
|
||||||
|
|
||||||
if !app.config.Server.ConcurrentListenersEnabled {
|
|
||||||
if app.config.Tailscale.Enabled {
|
|
||||||
l = append(l, ListenerTailscale)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.config.Server.SocketPath != "" {
|
|
||||||
l = append(l, ListenerUnix)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
l = append(l, ListenerHTTP)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.config.Server.SocketPath != "" {
|
|
||||||
l = append(l, ListenerUnix)
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.config.Tailscale.Enabled {
|
|
||||||
l = append(l, ListenerTailscale)
|
|
||||||
}
|
|
||||||
|
|
||||||
l = append(l, ListenerHTTP)
|
|
||||||
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *BootstrapApp) listenerFromType(listenerType Listener) (func() error, error) {
|
|
||||||
switch listenerType {
|
|
||||||
case ListenerHTTP:
|
|
||||||
return app.serveHTTP, nil
|
|
||||||
case ListenerUnix:
|
|
||||||
return app.serveUnix, nil
|
|
||||||
case ListenerTailscale:
|
|
||||||
return app.serveTailscale, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid listener type: %d", listenerType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *BootstrapApp) serveHTTP() error {
|
|
||||||
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
|
|
||||||
|
|
||||||
app.log.App.Info().Msgf("Starting server on %s", address)
|
|
||||||
|
|
||||||
listener, err := net.Listen("tcp", address)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create tcp listener: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
server := &http.Server{
|
|
||||||
Addr: address,
|
|
||||||
Handler: app.router.Handler(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return app.serve(listener, server, "http")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *BootstrapApp) serveUnix() error {
|
|
||||||
_, err := os.Stat(app.config.Server.SocketPath)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
app.log.App.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath)
|
|
||||||
err := os.Remove(app.config.Server.SocketPath)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to remove existing socket file: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.log.App.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath)
|
|
||||||
|
|
||||||
listener, err := net.Listen("unix", app.config.Server.SocketPath)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create unix socket listener: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
server := &http.Server{
|
|
||||||
Handler: app.router.Handler(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return app.serve(listener, server, "unix socket")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *BootstrapApp) serveTailscale() error {
|
|
||||||
app.log.App.Info().Msgf("Starting Tailscale server on %s", fmt.Sprintf("https://%s", app.services.tailscaleService.GetHostname()))
|
|
||||||
|
|
||||||
listener, err := app.services.tailscaleService.CreateListener()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create tailscale listener: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
server := &http.Server{
|
|
||||||
Handler: app.router.Handler(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return app.serve(listener, server, "tailscale")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, name string) error {
|
|
||||||
shutdown := func() {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), model.GracefulShutdownTimeout*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
err := server.Shutdown(ctx)
|
|
||||||
if err != nil {
|
|
||||||
app.log.App.Error().Err(err).Msgf("Failed to shutdown %s listener gracefully", name)
|
|
||||||
}
|
|
||||||
listener.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-app.ctx.Done()
|
|
||||||
app.log.App.Debug().Msgf("Shutting down %s listener", name)
|
|
||||||
shutdown()
|
|
||||||
}()
|
|
||||||
|
|
||||||
err := server.Serve(listener)
|
|
||||||
|
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
shutdown()
|
|
||||||
return fmt.Errorf("failed to start %s listener: %w", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package bootstrap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
@@ -17,33 +16,42 @@ func (app *BootstrapApp) setupServices() error {
|
|||||||
|
|
||||||
app.services.ldapService = ldapService
|
app.services.ldapService = ldapService
|
||||||
|
|
||||||
labelProvider, err := app.getLabelProvider()
|
useKubernetes := app.config.LabelProvider == "kubernetes" ||
|
||||||
|
(app.config.LabelProvider == "auto" && os.Getenv("KUBERNETES_SERVICE_HOST") != "")
|
||||||
|
|
||||||
if err != nil {
|
var labelProvider service.LabelProvider
|
||||||
return fmt.Errorf("failed to initialize label provider: %w", err)
|
|
||||||
|
if useKubernetes {
|
||||||
|
app.log.App.Debug().Msg("Using Kubernetes label provider")
|
||||||
|
|
||||||
|
kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, &app.wg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize kubernetes service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.services.kubernetesService = kubernetesService
|
||||||
|
labelProvider = kubernetesService
|
||||||
|
} else {
|
||||||
|
app.log.App.Debug().Msg("Using Docker label provider")
|
||||||
|
|
||||||
|
dockerService, err := service.NewDockerService(app.log, app.ctx, &app.wg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize docker service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.services.dockerService = dockerService
|
||||||
|
labelProvider = dockerService
|
||||||
}
|
}
|
||||||
|
|
||||||
tailscaleService, err := service.NewTailscaleService(app.log, app.config, app.ctx, &app.wg)
|
accessControlsService := service.NewAccessControlsService(app.log, &labelProvider, app.config.Apps)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
app.log.App.Warn().Err(err).Msg("Failed to initialize Tailscale connection, will continue without it")
|
|
||||||
}
|
|
||||||
|
|
||||||
app.services.tailscaleService = tailscaleService
|
|
||||||
|
|
||||||
accessControlsService := service.NewAccessControlsService(app.log, app.config, &labelProvider)
|
|
||||||
app.services.accessControlService = accessControlsService
|
app.services.accessControlService = accessControlsService
|
||||||
|
|
||||||
err = app.setupPolicyEngine()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize policy engine: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx)
|
oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx)
|
||||||
app.services.oauthBrokerService = oauthBrokerService
|
app.services.oauthBrokerService = oauthBrokerService
|
||||||
|
|
||||||
authService := service.NewAuthService(app.log, app.config, app.runtime, app.ctx, &app.wg, app.services.ldapService, app.queries, app.services.oauthBrokerService, app.services.tailscaleService)
|
authService := service.NewAuthService(app.log, app.config, app.runtime, app.ctx, &app.wg, app.services.ldapService, app.queries, app.services.oauthBrokerService)
|
||||||
app.services.authService = authService
|
app.services.authService = authService
|
||||||
|
|
||||||
oidcService, err := service.NewOIDCService(app.log, app.config, app.runtime, app.queries, app.ctx, &app.wg)
|
oidcService, err := service.NewOIDCService(app.log, app.config, app.runtime, app.queries, app.ctx, &app.wg)
|
||||||
@@ -56,79 +64,3 @@ func (app *BootstrapApp) setupServices() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
|
|
||||||
switch app.config.LabelProvider {
|
|
||||||
case "none", "docker", "kubernetes", "auto":
|
|
||||||
if app.config.LabelProvider == "none" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
useKubernetes := app.config.LabelProvider == "kubernetes" ||
|
|
||||||
(app.config.LabelProvider == "auto" && os.Getenv("KUBERNETES_SERVICE_HOST") != "")
|
|
||||||
|
|
||||||
if useKubernetes {
|
|
||||||
app.log.App.Debug().Msg("Using Kubernetes label provider")
|
|
||||||
|
|
||||||
kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, &app.wg)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to initialize kubernetes service: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.services.kubernetesService = kubernetesService
|
|
||||||
return kubernetesService, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
app.log.App.Debug().Msg("Using Docker label provider")
|
|
||||||
|
|
||||||
dockerService, err := service.NewDockerService(app.log, app.ctx, &app.wg)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to initialize docker service: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dockerService == nil {
|
|
||||||
if app.config.LabelProvider == "docker" {
|
|
||||||
app.log.App.Warn().Msg("Docker label provider selected but Docker is not available, will continue without it")
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
app.services.dockerService = dockerService
|
|
||||||
return dockerService, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid label provider: %s", app.config.LabelProvider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *BootstrapApp) setupPolicyEngine() error {
|
|
||||||
policyEngine, err := service.NewPolicyEngine(app.config, app.log)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize policy engine: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{
|
|
||||||
Log: app.log,
|
|
||||||
})
|
|
||||||
policyEngine.RegisterRule(service.RuleOAuthGroup, &service.OAuthGroupRule{
|
|
||||||
Log: app.log,
|
|
||||||
})
|
|
||||||
policyEngine.RegisterRule(service.RuleLDAPGroup, &service.LDAPGroupRule{
|
|
||||||
Log: app.log,
|
|
||||||
})
|
|
||||||
policyEngine.RegisterRule(service.RuleAuthEnabled, &service.AuthEnabledRule{
|
|
||||||
Log: app.log,
|
|
||||||
})
|
|
||||||
policyEngine.RegisterRule(service.RuleIPAllowed, &service.IPAllowedRule{
|
|
||||||
Log: app.log,
|
|
||||||
Config: app.config,
|
|
||||||
})
|
|
||||||
policyEngine.RegisterRule(service.RuleIPBypassed, &service.IPBypassedRule{
|
|
||||||
Log: app.log,
|
|
||||||
})
|
|
||||||
|
|
||||||
app.services.policyEngine = policyEngine
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,74 +1,39 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UCR -> User Context Response
|
|
||||||
|
|
||||||
type UCRAuth struct {
|
|
||||||
Authenticated bool `json:"authenticated"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
ProviderID string `json:"providerId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UCROAuth struct {
|
|
||||||
Active bool `json:"active"`
|
|
||||||
DisplayName string `json:"displayName"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UCRTOTP struct {
|
|
||||||
Pending bool `json:"pending"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UCRTailscale struct {
|
|
||||||
NodeName string `json:"nodeName,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserContextResponse struct {
|
type UserContextResponse struct {
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Auth UCRAuth `json:"auth"`
|
IsLoggedIn bool `json:"isLoggedIn"`
|
||||||
OAuth UCROAuth `json:"oauth"`
|
Username string `json:"username"`
|
||||||
TOTP UCRTOTP `json:"totp"`
|
Name string `json:"name"`
|
||||||
Tailscale UCRTailscale `json:"tailscale"`
|
Email string `json:"email"`
|
||||||
}
|
Provider string `json:"provider"`
|
||||||
|
OAuth bool `json:"oauth"`
|
||||||
// ACR -> App Context Response
|
TOTPPending bool `json:"totpPending"`
|
||||||
|
OAuthName string `json:"oauthName"`
|
||||||
type ACRAuth struct {
|
|
||||||
Providers []model.Provider `json:"providers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ACROAuth struct {
|
|
||||||
AutoRedirect string `json:"autoRedirect"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ACRUI struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
ForgotPasswordMessage string `json:"forgotPasswordMessage"`
|
|
||||||
BackgroundImage string `json:"backgroundImage"`
|
|
||||||
WarningsEnabled bool `json:"warningsEnabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ACRApp struct {
|
|
||||||
AppURL string `json:"appUrl"`
|
|
||||||
CookieDomain string `json:"cookieDomain"`
|
|
||||||
TrustedDomains []string `json:"trustedDomains"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppContextResponse struct {
|
type AppContextResponse struct {
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Auth ACRAuth `json:"auth"`
|
Providers []model.Provider `json:"providers"`
|
||||||
OAuth ACROAuth `json:"oauth"`
|
Title string `json:"title"`
|
||||||
UI ACRUI `json:"ui"`
|
AppURL string `json:"appUrl"`
|
||||||
App ACRApp `json:"app"`
|
CookieDomain string `json:"cookieDomain"`
|
||||||
|
ForgotPasswordMessage string `json:"forgotPasswordMessage"`
|
||||||
|
BackgroundImage string `json:"backgroundImage"`
|
||||||
|
OAuthAutoRedirect string `json:"oauthAutoRedirect"`
|
||||||
|
WarningsEnabled bool `json:"warningsEnabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContextController struct {
|
type ContextController struct {
|
||||||
@@ -106,58 +71,51 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to create user context from request")
|
controller.log.App.Error().Err(err).Msg("Failed to create user context from request")
|
||||||
c.JSON(200, UserContextResponse{
|
c.JSON(200, UserContextResponse{
|
||||||
Status: 401,
|
Status: 401,
|
||||||
Message: "Unauthorized",
|
Message: "Unauthorized",
|
||||||
Auth: UCRAuth{Authenticated: false},
|
IsLoggedIn: false,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userContext := UserContextResponse{
|
userContext := UserContextResponse{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Message: "Success",
|
Message: "Success",
|
||||||
Auth: UCRAuth{
|
IsLoggedIn: context.Authenticated,
|
||||||
Authenticated: context.Authenticated,
|
Username: context.GetUsername(),
|
||||||
Username: context.GetUsername(),
|
Name: context.GetName(),
|
||||||
Name: context.GetName(),
|
Email: context.GetEmail(),
|
||||||
Email: context.GetEmail(),
|
Provider: context.GetProviderID(),
|
||||||
ProviderID: context.GetProviderID(),
|
OAuth: context.IsOAuth(),
|
||||||
},
|
TOTPPending: context.TOTPPending(),
|
||||||
OAuth: UCROAuth{
|
OAuthName: context.OAuthName(),
|
||||||
Active: context.IsOAuth(),
|
|
||||||
DisplayName: context.OAuthName(),
|
|
||||||
},
|
|
||||||
TOTP: UCRTOTP{
|
|
||||||
Pending: context.TOTPPending(),
|
|
||||||
},
|
|
||||||
Tailscale: UCRTailscale{
|
|
||||||
NodeName: context.TailscaleNodeName(),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(200, userContext)
|
c.JSON(200, userContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *ContextController) appContextHandler(c *gin.Context) {
|
func (controller *ContextController) appContextHandler(c *gin.Context) {
|
||||||
|
appUrl, err := url.Parse(controller.runtime.AppURL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
controller.log.App.Error().Err(err).Msg("Failed to parse app URL")
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"status": 500,
|
||||||
|
"message": "Internal Server Error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(200, AppContextResponse{
|
c.JSON(200, AppContextResponse{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Message: "Success",
|
Message: "Success",
|
||||||
Auth: ACRAuth{
|
Providers: controller.runtime.ConfiguredProviders,
|
||||||
Providers: controller.runtime.ConfiguredProviders,
|
Title: controller.config.UI.Title,
|
||||||
},
|
AppURL: fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host),
|
||||||
OAuth: ACROAuth{
|
CookieDomain: controller.runtime.CookieDomain,
|
||||||
AutoRedirect: controller.config.OAuth.AutoRedirect,
|
ForgotPasswordMessage: controller.config.UI.ForgotPasswordMessage,
|
||||||
},
|
BackgroundImage: controller.config.UI.BackgroundImage,
|
||||||
UI: ACRUI{
|
OAuthAutoRedirect: controller.config.OAuth.AutoRedirect,
|
||||||
Title: controller.config.UI.Title,
|
WarningsEnabled: controller.config.UI.WarningsEnabled,
|
||||||
ForgotPasswordMessage: controller.config.UI.ForgotPasswordMessage,
|
|
||||||
BackgroundImage: controller.config.UI.BackgroundImage,
|
|
||||||
WarningsEnabled: controller.config.UI.WarningsEnabled,
|
|
||||||
},
|
|
||||||
App: ACRApp{
|
|
||||||
AppURL: controller.runtime.AppURL,
|
|
||||||
CookieDomain: controller.runtime.CookieDomain,
|
|
||||||
TrustedDomains: controller.runtime.TrustedDomains,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,25 +34,16 @@ func TestContextController(t *testing.T) {
|
|||||||
path: "/api/context/app",
|
path: "/api/context/app",
|
||||||
expected: func() string {
|
expected: func() string {
|
||||||
expectedAppContextResponse := controller.AppContextResponse{
|
expectedAppContextResponse := controller.AppContextResponse{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Message: "Success",
|
Message: "Success",
|
||||||
Auth: controller.ACRAuth{
|
Providers: runtime.ConfiguredProviders,
|
||||||
Providers: runtime.ConfiguredProviders,
|
Title: cfg.UI.Title,
|
||||||
},
|
AppURL: runtime.AppURL,
|
||||||
OAuth: controller.ACROAuth{
|
CookieDomain: runtime.CookieDomain,
|
||||||
AutoRedirect: cfg.OAuth.AutoRedirect,
|
ForgotPasswordMessage: cfg.UI.ForgotPasswordMessage,
|
||||||
},
|
BackgroundImage: cfg.UI.BackgroundImage,
|
||||||
UI: controller.ACRUI{
|
OAuthAutoRedirect: cfg.OAuth.AutoRedirect,
|
||||||
Title: cfg.UI.Title,
|
WarningsEnabled: cfg.UI.WarningsEnabled,
|
||||||
ForgotPasswordMessage: cfg.UI.ForgotPasswordMessage,
|
|
||||||
BackgroundImage: cfg.UI.BackgroundImage,
|
|
||||||
WarningsEnabled: cfg.UI.WarningsEnabled,
|
|
||||||
},
|
|
||||||
App: controller.ACRApp{
|
|
||||||
AppURL: runtime.AppURL,
|
|
||||||
CookieDomain: runtime.CookieDomain,
|
|
||||||
TrustedDomains: runtime.TrustedDomains,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
bytes, err := json.Marshal(expectedAppContextResponse)
|
bytes, err := json.Marshal(expectedAppContextResponse)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -93,15 +84,13 @@ func TestContextController(t *testing.T) {
|
|||||||
path: "/api/context/user",
|
path: "/api/context/user",
|
||||||
expected: func() string {
|
expected: func() string {
|
||||||
expectedUserContextResponse := controller.UserContextResponse{
|
expectedUserContextResponse := controller.UserContextResponse{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Message: "Success",
|
Message: "Success",
|
||||||
Auth: controller.UCRAuth{
|
IsLoggedIn: true,
|
||||||
Authenticated: true,
|
Username: "johndoe",
|
||||||
Username: "johndoe",
|
Name: "John Doe",
|
||||||
Name: "John Doe",
|
Email: utils.CompileUserEmail("johndoe", runtime.CookieDomain),
|
||||||
Email: utils.CompileUserEmail("johndoe", runtime.CookieDomain),
|
Provider: "local",
|
||||||
ProviderID: "local",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
bytes, err := json.Marshal(expectedUserContextResponse)
|
bytes, err := json.Marshal(expectedUserContextResponse)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ import (
|
|||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/test"
|
"github.com/tinyauthapp/tinyauth/internal/test"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
@@ -838,11 +839,16 @@ func TestOIDCController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
store := memory.New()
|
app := bootstrap.NewBootstrapApp(cfg)
|
||||||
|
|
||||||
|
err := app.SetupDatabase()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
queries := repository.New(app.GetDB())
|
||||||
|
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
oidcService, err := service.NewOIDCService(log, cfg, runtime, store, context.TODO(), wg)
|
oidcService, err := service.NewOIDCService(log, cfg, runtime, queries, context.TODO(), wg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@@ -863,4 +869,8 @@ func TestOIDCController(t *testing.T) {
|
|||||||
test.run(t, router, recorder)
|
test.run(t, router, recorder)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
app.GetDB().Close()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -52,11 +51,10 @@ type ProxyContext struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProxyController struct {
|
type ProxyController struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
runtime model.RuntimeConfig
|
runtime model.RuntimeConfig
|
||||||
acls *service.AccessControlsService
|
acls *service.AccessControlsService
|
||||||
auth *service.AuthService
|
auth *service.AuthService
|
||||||
policyEngine *service.PolicyEngine
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxyController(
|
func NewProxyController(
|
||||||
@@ -65,14 +63,12 @@ func NewProxyController(
|
|||||||
router *gin.RouterGroup,
|
router *gin.RouterGroup,
|
||||||
acls *service.AccessControlsService,
|
acls *service.AccessControlsService,
|
||||||
auth *service.AuthService,
|
auth *service.AuthService,
|
||||||
policyEngine *service.PolicyEngine,
|
|
||||||
) *ProxyController {
|
) *ProxyController {
|
||||||
controller := &ProxyController{
|
controller := &ProxyController{
|
||||||
log: log,
|
log: log,
|
||||||
runtime: runtime,
|
runtime: runtime,
|
||||||
acls: acls,
|
acls: acls,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
policyEngine: policyEngine,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyGroup := router.Group("/auth")
|
proxyGroup := router.Group("/auth")
|
||||||
@@ -105,13 +101,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
clientIP := c.ClientIP()
|
clientIP := c.ClientIP()
|
||||||
|
|
||||||
aclsCtx := &service.ACLContext{
|
if controller.auth.IsBypassedIP(clientIP, acls) {
|
||||||
ACLs: acls,
|
|
||||||
IP: net.ParseIP(clientIP),
|
|
||||||
Path: proxyCtx.Path,
|
|
||||||
}
|
|
||||||
|
|
||||||
if controller.policyEngine.Evaluate(service.RuleIPBypassed, aclsCtx) {
|
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, acls)
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -120,7 +110,15 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if controller.policyEngine.Evaluate(service.RuleAuthEnabled, aclsCtx) {
|
authEnabled, err := controller.auth.IsAuthEnabled(proxyCtx.Path, acls)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
controller.log.App.Error().Err(err).Msg("Failed to determine if authentication is enabled for resource")
|
||||||
|
controller.handleError(c, proxyCtx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !authEnabled {
|
||||||
controller.log.App.Debug().Msg("Authentication is disabled for this resource, allowing access without authentication")
|
controller.log.App.Debug().Msg("Authentication is disabled for this resource, allowing access without authentication")
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, acls)
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
@@ -130,7 +128,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !controller.policyEngine.Evaluate(service.RuleIPAllowed, aclsCtx) {
|
if !controller.auth.CheckIP(clientIP, acls) {
|
||||||
queries, err := query.Values(UnauthorizedQuery{
|
queries, err := query.Values(UnauthorizedQuery{
|
||||||
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
||||||
IP: clientIP,
|
IP: clientIP,
|
||||||
@@ -166,10 +164,10 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
aclsCtx.UserContext = userContext
|
|
||||||
|
|
||||||
if userContext.Authenticated {
|
if userContext.Authenticated {
|
||||||
if !controller.policyEngine.Evaluate(service.RuleUserAllowed, aclsCtx) {
|
userAllowed := controller.auth.IsUserAllowed(c, *userContext, acls)
|
||||||
|
|
||||||
|
if !userAllowed {
|
||||||
controller.log.App.Warn().Str("user", userContext.GetUsername()).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User is not allowed to access resource")
|
controller.log.App.Warn().Str("user", userContext.GetUsername()).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User is not allowed to access resource")
|
||||||
|
|
||||||
queries, err := query.Values(UnauthorizedQuery{
|
queries, err := query.Values(UnauthorizedQuery{
|
||||||
@@ -207,9 +205,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
var groupOK bool
|
var groupOK bool
|
||||||
|
|
||||||
if userContext.IsOAuth() {
|
if userContext.IsOAuth() {
|
||||||
groupOK = controller.policyEngine.Evaluate(service.RuleOAuthGroup, aclsCtx)
|
groupOK = controller.auth.IsInOAuthGroup(c, *userContext, acls)
|
||||||
} else {
|
} else {
|
||||||
groupOK = controller.policyEngine.Evaluate(service.RuleLDAPGroup, aclsCtx)
|
groupOK = controller.auth.IsInLDAPGroup(c, *userContext, acls)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !groupOK {
|
if !groupOK {
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/test"
|
"github.com/tinyauthapp/tinyauth/internal/test"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
@@ -23,6 +24,33 @@ func TestProxyController(t *testing.T) {
|
|||||||
|
|
||||||
cfg, runtime := test.CreateTestConfigs(t)
|
cfg, runtime := test.CreateTestConfigs(t)
|
||||||
|
|
||||||
|
acls := map[string]model.App{
|
||||||
|
"app_path_allow": {
|
||||||
|
Config: model.AppConfig{
|
||||||
|
Domain: "path-allow.example.com",
|
||||||
|
},
|
||||||
|
Path: model.AppPath{
|
||||||
|
Allow: "/allowed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"app_user_allow": {
|
||||||
|
Config: model.AppConfig{
|
||||||
|
Domain: "user-allow.example.com",
|
||||||
|
},
|
||||||
|
Users: model.AppUsers{
|
||||||
|
Allow: "testuser",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ip_bypass": {
|
||||||
|
Config: model.AppConfig{
|
||||||
|
Domain: "ip-bypass.example.com",
|
||||||
|
},
|
||||||
|
IP: model.AppIP{
|
||||||
|
Bypass: []string{"10.10.10.10"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const browserUserAgent = `
|
const browserUserAgent = `
|
||||||
Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36`
|
Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36`
|
||||||
|
|
||||||
@@ -351,37 +379,19 @@ func TestProxyController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
store := memory.New()
|
app := bootstrap.NewBootstrapApp(cfg)
|
||||||
|
|
||||||
|
err := app.SetupDatabase()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
queries := repository.New(app.GetDB())
|
||||||
|
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
|
|
||||||
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
|
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
|
||||||
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, store, broker, nil)
|
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, queries, broker)
|
||||||
aclsService := service.NewAccessControlsService(log, cfg, nil)
|
aclsService := service.NewAccessControlsService(log, nil, acls)
|
||||||
|
|
||||||
policyEngine, err := service.NewPolicyEngine(cfg, log)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{
|
|
||||||
Log: log,
|
|
||||||
})
|
|
||||||
policyEngine.RegisterRule(service.RuleOAuthGroup, &service.OAuthGroupRule{
|
|
||||||
Log: log,
|
|
||||||
})
|
|
||||||
policyEngine.RegisterRule(service.RuleLDAPGroup, &service.LDAPGroupRule{
|
|
||||||
Log: log,
|
|
||||||
})
|
|
||||||
policyEngine.RegisterRule(service.RuleAuthEnabled, &service.AuthEnabledRule{
|
|
||||||
Log: log,
|
|
||||||
})
|
|
||||||
policyEngine.RegisterRule(service.RuleIPAllowed, &service.IPAllowedRule{
|
|
||||||
Log: log,
|
|
||||||
Config: cfg,
|
|
||||||
})
|
|
||||||
policyEngine.RegisterRule(service.RuleIPBypassed, &service.IPBypassedRule{
|
|
||||||
Log: log,
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.description, func(t *testing.T) {
|
t.Run(test.description, func(t *testing.T) {
|
||||||
@@ -396,9 +406,13 @@ func TestProxyController(t *testing.T) {
|
|||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
controller.NewProxyController(log, runtime, group, aclsService, authService, policyEngine)
|
controller.NewProxyController(log, runtime, group, aclsService, authService)
|
||||||
|
|
||||||
test.run(t, router, recorder)
|
test.run(t, router, recorder)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
app.GetDB().Close()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ func NewUserController(
|
|||||||
userGroup.POST("/login", controller.loginHandler)
|
userGroup.POST("/login", controller.loginHandler)
|
||||||
userGroup.POST("/logout", controller.logoutHandler)
|
userGroup.POST("/logout", controller.logoutHandler)
|
||||||
userGroup.POST("/totp", controller.totpHandler)
|
userGroup.POST("/totp", controller.totpHandler)
|
||||||
userGroup.POST("/tailscale", controller.tailscaleHandler)
|
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
@@ -395,53 +394,3 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
"message": "Login successful",
|
"message": "Login successful",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *UserController) tailscaleHandler(c *gin.Context) {
|
|
||||||
context, err := new(model.UserContext).NewFromGin(c)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to create user context from request")
|
|
||||||
c.JSON(401, gin.H{
|
|
||||||
"status": 401,
|
|
||||||
"message": "Unauthorized",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if context.Tailscale == nil {
|
|
||||||
controller.log.App.Warn().Msg("Tailscale login attempt without Tailscale context")
|
|
||||||
c.JSON(401, gin.H{
|
|
||||||
"status": 401,
|
|
||||||
"message": "Unauthorized",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionCookie := repository.Session{
|
|
||||||
Username: context.Tailscale.Username,
|
|
||||||
Name: context.Tailscale.Name,
|
|
||||||
Email: context.Tailscale.Email,
|
|
||||||
Provider: "tailscale",
|
|
||||||
}
|
|
||||||
|
|
||||||
cookie, err := controller.auth.CreateSession(c, sessionCookie)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
controller.log.App.Error().Err(err).Str("username", context.GetUsername()).Msg("Failed to create session cookie after successful Tailscale login")
|
|
||||||
c.JSON(500, gin.H{
|
|
||||||
"status": 500,
|
|
||||||
"message": "Internal Server Error",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.SetCookie(c.Writer, cookie)
|
|
||||||
|
|
||||||
controller.log.App.Info().Str("username", context.GetUsername()).Msg("Tailscale login successful, login complete")
|
|
||||||
controller.log.AuditLoginSuccess(context.GetUsername(), "tailscale", c.ClientIP())
|
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"status": 200,
|
|
||||||
"message": "Login successful",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import (
|
|||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/test"
|
"github.com/tinyauthapp/tinyauth/internal/test"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
@@ -73,7 +73,12 @@ func TestUserController(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
store := memory.New()
|
app := bootstrap.NewBootstrapApp(cfg)
|
||||||
|
|
||||||
|
err := app.SetupDatabase()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
queries := repository.New(app.GetDB())
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
description string
|
description string
|
||||||
@@ -249,7 +254,7 @@ func TestUserController(t *testing.T) {
|
|||||||
totpCtx,
|
totpCtx,
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
_, err := store.CreateSession(context.TODO(), repository.CreateSessionParams{
|
_, err := queries.CreateSession(context.TODO(), repository.CreateSessionParams{
|
||||||
UUID: "test-totp-login-uuid",
|
UUID: "test-totp-login-uuid",
|
||||||
Username: "test",
|
Username: "test",
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
@@ -373,7 +378,7 @@ func TestUserController(t *testing.T) {
|
|||||||
totpAttrCtx,
|
totpAttrCtx,
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
_, err := store.CreateSession(context.TODO(), repository.CreateSessionParams{
|
_, err := queries.CreateSession(context.TODO(), repository.CreateSessionParams{
|
||||||
UUID: "test-totp-login-attributes-uuid",
|
UUID: "test-totp-login-attributes-uuid",
|
||||||
Username: "test",
|
Username: "test",
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
@@ -415,7 +420,7 @@ func TestUserController(t *testing.T) {
|
|||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
|
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
|
||||||
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, store, broker, nil)
|
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, queries, broker)
|
||||||
|
|
||||||
beforeEach := func() {
|
beforeEach := func() {
|
||||||
// Clear failed login attempts before each test
|
// Clear failed login attempts before each test
|
||||||
@@ -441,4 +446,8 @@ func TestUserController(t *testing.T) {
|
|||||||
test.run(t, router, recorder)
|
test.run(t, router, recorder)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
app.GetDB().Close()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/test"
|
"github.com/tinyauthapp/tinyauth/internal/test"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
@@ -91,9 +92,14 @@ func TestWellKnownController(t *testing.T) {
|
|||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
store := memory.New()
|
app := bootstrap.NewBootstrapApp(cfg)
|
||||||
|
|
||||||
oidcService, err := service.NewOIDCService(log, cfg, runtime, store, ctx, wg)
|
err := app.SetupDatabase()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
queries := repository.New(app.GetDB())
|
||||||
|
|
||||||
|
oidcService, err := service.NewOIDCService(log, cfg, runtime, queries, ctx, wg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@@ -108,4 +114,8 @@ func TestWellKnownController(t *testing.T) {
|
|||||||
test.run(t, router, recorder)
|
test.run(t, router, recorder)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
app.GetDB().Close()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,11 +36,10 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ContextMiddleware struct {
|
type ContextMiddleware struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
runtime model.RuntimeConfig
|
runtime model.RuntimeConfig
|
||||||
auth *service.AuthService
|
auth *service.AuthService
|
||||||
broker *service.OAuthBrokerService
|
broker *service.OAuthBrokerService
|
||||||
tailscale *service.TailscaleService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContextMiddleware(
|
func NewContextMiddleware(
|
||||||
@@ -48,14 +47,12 @@ func NewContextMiddleware(
|
|||||||
runtime model.RuntimeConfig,
|
runtime model.RuntimeConfig,
|
||||||
auth *service.AuthService,
|
auth *service.AuthService,
|
||||||
broker *service.OAuthBrokerService,
|
broker *service.OAuthBrokerService,
|
||||||
tailscale *service.TailscaleService,
|
|
||||||
) *ContextMiddleware {
|
) *ContextMiddleware {
|
||||||
return &ContextMiddleware{
|
return &ContextMiddleware{
|
||||||
log: log,
|
log: log,
|
||||||
runtime: runtime,
|
runtime: runtime,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
broker: broker,
|
broker: broker,
|
||||||
tailscale: tailscale,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +66,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
uuid, err := c.Cookie(m.runtime.SessionCookieName)
|
uuid, err := c.Cookie(m.runtime.SessionCookieName)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
userContext, cookie, err := m.cookieAuth(c.Request.Context(), uuid, c.RemoteIP())
|
userContext, cookie, err := m.cookieAuth(c.Request.Context(), uuid)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if cookie != nil {
|
if cookie != nil {
|
||||||
@@ -105,28 +102,11 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lastly check if we have a tailscale session to add
|
|
||||||
if m.tailscale != nil {
|
|
||||||
tailscaleContext, err := m.tailscaleWhois(c.Request.Context(), c.RemoteIP())
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
m.log.App.Error().Err(err).Msgf("Error performing tailscale whois for IP %s: %v", c.RemoteIP(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tailscaleContext != nil {
|
|
||||||
c.Set("context", &model.UserContext{
|
|
||||||
Authenticated: false,
|
|
||||||
Provider: model.ProviderTailscale,
|
|
||||||
Tailscale: tailscaleContext,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string, ip string) (*model.UserContext, *http.Cookie, error) {
|
func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model.UserContext, *http.Cookie, error) {
|
||||||
session, err := m.auth.GetSession(ctx, uuid)
|
session, err := m.auth.GetSession(ctx, uuid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -161,18 +141,6 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string, ip stri
|
|||||||
if userContext.Local.Attributes.Email == "" {
|
if userContext.Local.Attributes.Email == "" {
|
||||||
userContext.Local.Attributes.Email = utils.CompileUserEmail(user.Username, m.runtime.CookieDomain)
|
userContext.Local.Attributes.Email = utils.CompileUserEmail(user.Username, m.runtime.CookieDomain)
|
||||||
}
|
}
|
||||||
case model.ProviderTailscale:
|
|
||||||
tailscaleContext, err := m.tailscaleWhois(ctx, ip)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("error performing tailscale whois: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tailscaleContext == nil {
|
|
||||||
return nil, nil, fmt.Errorf("tailscale whois returned no result for IP: %s", ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
userContext.Tailscale = tailscaleContext
|
|
||||||
case model.ProviderLDAP:
|
case model.ProviderLDAP:
|
||||||
search, err := m.auth.SearchUser(userContext.LDAP.Username)
|
search, err := m.auth.SearchUser(userContext.LDAP.Username)
|
||||||
|
|
||||||
@@ -298,36 +266,3 @@ func (m *ContextMiddleware) isIgnorePath(path string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ContextMiddleware) tailscaleWhois(ctx context.Context, ip string) (*model.TailscaleContext, error) {
|
|
||||||
if m.tailscale == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
whois, err := m.tailscale.Whois(ctx, ip)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
m.log.App.Error().Err(err).Msgf("Error performing Tailscale whois for IP %s: %v", ip, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if whois == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
uctx := model.TailscaleContext{
|
|
||||||
BaseContext: model.BaseContext{
|
|
||||||
Username: whois.NodeName,
|
|
||||||
Email: whois.LoginName,
|
|
||||||
Name: whois.DisplayName,
|
|
||||||
},
|
|
||||||
UserID: whois.UserID,
|
|
||||||
Tags: whois.Tags,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.ContainsAny(uctx.Email, "@") {
|
|
||||||
uctx.Email = utils.CompileUserEmail(uctx.Email+"-tailscale", m.runtime.CookieDomain)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &uctx, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/middleware"
|
"github.com/tinyauthapp/tinyauth/internal/middleware"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/test"
|
"github.com/tinyauthapp/tinyauth/internal/test"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
@@ -31,7 +31,7 @@ func TestContextMiddleware(t *testing.T) {
|
|||||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
|
return "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
|
||||||
}
|
}
|
||||||
|
|
||||||
seedSession := func(t *testing.T, queries repository.Store, params repository.CreateSessionParams) {
|
seedSession := func(t *testing.T, queries *repository.Queries, params repository.CreateSessionParams) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
_, err := queries.CreateSession(context.Background(), params)
|
_, err := queries.CreateSession(context.Background(), params)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -39,7 +39,7 @@ func TestContextMiddleware(t *testing.T) {
|
|||||||
|
|
||||||
type runArgs struct {
|
type runArgs struct {
|
||||||
do func(req *http.Request) (*model.UserContext, *httptest.ResponseRecorder)
|
do func(req *http.Request) (*model.UserContext, *httptest.ResponseRecorder)
|
||||||
queries repository.Store
|
queries *repository.Queries
|
||||||
}
|
}
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
@@ -252,12 +252,17 @@ func TestContextMiddleware(t *testing.T) {
|
|||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
store := memory.New()
|
app := bootstrap.NewBootstrapApp(cfg)
|
||||||
|
|
||||||
|
err := app.SetupDatabase()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
queries := repository.New(app.GetDB())
|
||||||
|
|
||||||
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
|
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
|
||||||
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, store, broker, nil)
|
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, queries, broker)
|
||||||
|
|
||||||
contextMiddleware := middleware.NewContextMiddleware(log, runtime, authService, broker, nil)
|
contextMiddleware := middleware.NewContextMiddleware(log, runtime, authService, broker)
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
authService.ClearRateLimitsTestingOnly()
|
authService.ClearRateLimitsTestingOnly()
|
||||||
@@ -281,7 +286,11 @@ func TestContextMiddleware(t *testing.T) {
|
|||||||
return captured, recorder
|
return captured, recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
test.run(t, runArgs{do: do, queries: store})
|
test.run(t, runArgs{do: do, queries: queries})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
app.GetDB().Close()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ package model
|
|||||||
func NewDefaultConfiguration() *Config {
|
func NewDefaultConfiguration() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Database: DatabaseConfig{
|
Database: DatabaseConfig{
|
||||||
Driver: "sqlite",
|
Path: "./tinyauth.db",
|
||||||
Path: "./tinyauth.db",
|
|
||||||
},
|
},
|
||||||
Analytics: AnalyticsConfig{
|
Analytics: AnalyticsConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
@@ -25,9 +24,6 @@ func NewDefaultConfiguration() *Config {
|
|||||||
SessionMaxLifetime: 0, // disabled
|
SessionMaxLifetime: 0, // disabled
|
||||||
LoginTimeout: 300, // 5 minutes
|
LoginTimeout: 300, // 5 minutes
|
||||||
LoginMaxRetries: 3,
|
LoginMaxRetries: 3,
|
||||||
ACLs: ACLsConfig{
|
|
||||||
Policy: "allow",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
UI: UIConfig{
|
UI: UIConfig{
|
||||||
Title: "Tinyauth",
|
Title: "Tinyauth",
|
||||||
@@ -65,9 +61,6 @@ func NewDefaultConfiguration() *Config {
|
|||||||
Experimental: ExperimentalConfig{
|
Experimental: ExperimentalConfig{
|
||||||
ConfigFile: "",
|
ConfigFile: "",
|
||||||
},
|
},
|
||||||
Tailscale: TailscaleConfig{
|
|
||||||
Dir: "./tailscale_state",
|
|
||||||
},
|
|
||||||
LabelProvider: "auto",
|
LabelProvider: "auto",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,14 +78,12 @@ type Config struct {
|
|||||||
UI UIConfig `description:"UI customization." yaml:"ui"`
|
UI UIConfig `description:"UI customization." yaml:"ui"`
|
||||||
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap"`
|
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap"`
|
||||||
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
||||||
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider"`
|
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, or kubernetes). auto detects the environment." yaml:"labelProvider"`
|
||||||
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
||||||
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DatabaseConfig struct {
|
type DatabaseConfig struct {
|
||||||
Driver string `description:"The database driver to use. Valid values: sqlite, memory." yaml:"driver"`
|
Path string `description:"The path to the database, including file name." yaml:"path"`
|
||||||
Path string `description:"The path to the SQLite database, including file name. Only used when driver is sqlite." yaml:"path"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnalyticsConfig struct {
|
type AnalyticsConfig struct {
|
||||||
@@ -123,7 +114,6 @@ type AuthConfig struct {
|
|||||||
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
||||||
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
||||||
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
||||||
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserAttributes struct {
|
type UserAttributes struct {
|
||||||
@@ -211,16 +201,6 @@ type ExperimentalConfig struct {
|
|||||||
ConfigFile string `description:"Path to config file." yaml:"-"`
|
ConfigFile string `description:"Path to config file." yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TailscaleConfig struct {
|
|
||||||
Enabled bool `description:"Enable Tailscale integration." yaml:"enabled"`
|
|
||||||
Dir string `description:"Tailscale state directory." yaml:"dir"`
|
|
||||||
Hostname string `description:"Tailscale hostname." yaml:"hostname"`
|
|
||||||
AuthKey string `description:"Tailscale auth key." yaml:"authKey"`
|
|
||||||
Ephemeral bool `description:"Use ephemeral Tailscale node." yaml:"ephemeral"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OAuth/OIDC config
|
|
||||||
|
|
||||||
type OAuthServiceConfig struct {
|
type OAuthServiceConfig struct {
|
||||||
ClientID string `description:"OAuth client ID." yaml:"clientId"`
|
ClientID string `description:"OAuth client ID." yaml:"clientId"`
|
||||||
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret"`
|
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret"`
|
||||||
@@ -243,10 +223,6 @@ type OIDCClientConfig struct {
|
|||||||
Name string `description:"Client name in UI." yaml:"name"`
|
Name string `description:"Client name in UI." yaml:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ACLsConfig struct {
|
|
||||||
Policy string `description:"ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow." yaml:"policy"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ACLs
|
// ACLs
|
||||||
|
|
||||||
type Apps struct {
|
type Apps struct {
|
||||||
|
|||||||
@@ -21,5 +21,3 @@ const SessionCookieName = "tinyauth-session"
|
|||||||
const CSRFCookieName = "tinyauth-csrf"
|
const CSRFCookieName = "tinyauth-csrf"
|
||||||
const RedirectCookieName = "tinyauth-redirect"
|
const RedirectCookieName = "tinyauth-redirect"
|
||||||
const OAuthSessionCookieName = "tinyauth-oauth"
|
const OAuthSessionCookieName = "tinyauth-oauth"
|
||||||
|
|
||||||
const GracefulShutdownTimeout = 5 // seconds
|
|
||||||
|
|||||||
+58
-59
@@ -19,7 +19,6 @@ const (
|
|||||||
ProviderBasicAuth
|
ProviderBasicAuth
|
||||||
ProviderOAuth
|
ProviderOAuth
|
||||||
ProviderLDAP
|
ProviderLDAP
|
||||||
ProviderTailscale
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserContext struct {
|
type UserContext struct {
|
||||||
@@ -28,7 +27,6 @@ type UserContext struct {
|
|||||||
Local *LocalContext
|
Local *LocalContext
|
||||||
OAuth *OAuthContext
|
OAuth *OAuthContext
|
||||||
LDAP *LDAPContext
|
LDAP *LDAPContext
|
||||||
Tailscale *TailscaleContext
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseContext struct {
|
type BaseContext struct {
|
||||||
@@ -56,13 +54,6 @@ type LDAPContext struct {
|
|||||||
Groups []string
|
Groups []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type TailscaleContext struct {
|
|
||||||
BaseContext
|
|
||||||
UserID string
|
|
||||||
// for future use
|
|
||||||
Tags []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UserContext) IsAuthenticated() bool {
|
func (c *UserContext) IsAuthenticated() bool {
|
||||||
return c.Authenticated
|
return c.Authenticated
|
||||||
}
|
}
|
||||||
@@ -83,10 +74,6 @@ func (c *UserContext) IsBasicAuth() bool {
|
|||||||
return c.Provider == ProviderBasicAuth && c.Local != nil
|
return c.Provider == ProviderBasicAuth && c.Local != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) IsTailscale() bool {
|
|
||||||
return c.Provider == ProviderTailscale && c.Tailscale != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UserContext) NewFromGin(ginctx *gin.Context) (*UserContext, error) {
|
func (c *UserContext) NewFromGin(ginctx *gin.Context) (*UserContext, error) {
|
||||||
userContextValue, exists := ginctx.Get("context")
|
userContextValue, exists := ginctx.Get("context")
|
||||||
|
|
||||||
@@ -100,7 +87,7 @@ func (c *UserContext) NewFromGin(ginctx *gin.Context) (*UserContext, error) {
|
|||||||
return nil, errors.New("invalid user context type")
|
return nil, errors.New("invalid user context type")
|
||||||
}
|
}
|
||||||
|
|
||||||
if userContext.LDAP == nil && userContext.Local == nil && userContext.OAuth == nil && userContext.Tailscale == nil {
|
if userContext.LDAP == nil && userContext.Local == nil && userContext.OAuth == nil {
|
||||||
return nil, errors.New("incomplete user context")
|
return nil, errors.New("incomplete user context")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,15 +121,6 @@ func (c *UserContext) NewFromSession(session *repository.Session) (*UserContext,
|
|||||||
Email: session.Email,
|
Email: session.Email,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case "tailscale":
|
|
||||||
c.Provider = ProviderTailscale
|
|
||||||
c.Tailscale = &TailscaleContext{
|
|
||||||
BaseContext: BaseContext{
|
|
||||||
Username: session.Username,
|
|
||||||
Name: session.Name,
|
|
||||||
Email: session.Email,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// By default we assume an unknown name which is oauth
|
// By default we assume an unknown name which is oauth
|
||||||
default:
|
default:
|
||||||
c.Provider = ProviderOAuth
|
c.Provider = ProviderOAuth
|
||||||
@@ -167,55 +145,85 @@ func (c *UserContext) NewFromSession(session *repository.Session) (*UserContext,
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) getBaseContext() *BaseContext {
|
func (c *UserContext) GetUsername() string {
|
||||||
switch c.Provider {
|
switch c.Provider {
|
||||||
case ProviderLocal, ProviderBasicAuth:
|
case ProviderLocal:
|
||||||
if c.Local == nil {
|
if c.Local == nil {
|
||||||
return nil
|
return ""
|
||||||
}
|
}
|
||||||
return &c.Local.BaseContext
|
return c.Local.Username
|
||||||
case ProviderLDAP:
|
case ProviderLDAP:
|
||||||
if c.LDAP == nil {
|
if c.LDAP == nil {
|
||||||
return nil
|
return ""
|
||||||
}
|
}
|
||||||
return &c.LDAP.BaseContext
|
return c.LDAP.Username
|
||||||
|
case ProviderBasicAuth:
|
||||||
|
if c.Local == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.Local.Username
|
||||||
case ProviderOAuth:
|
case ProviderOAuth:
|
||||||
if c.OAuth == nil {
|
if c.OAuth == nil {
|
||||||
return nil
|
return ""
|
||||||
}
|
}
|
||||||
return &c.OAuth.BaseContext
|
return c.OAuth.Username
|
||||||
case ProviderTailscale:
|
|
||||||
if c.Tailscale == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &c.Tailscale.BaseContext
|
|
||||||
default:
|
default:
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UserContext) GetUsername() string {
|
|
||||||
base := c.getBaseContext()
|
|
||||||
if base == nil {
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return base.Username
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) GetEmail() string {
|
func (c *UserContext) GetEmail() string {
|
||||||
base := c.getBaseContext()
|
switch c.Provider {
|
||||||
if base == nil {
|
case ProviderLocal:
|
||||||
|
if c.Local == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.Local.Email
|
||||||
|
case ProviderLDAP:
|
||||||
|
if c.LDAP == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.LDAP.Email
|
||||||
|
case ProviderBasicAuth:
|
||||||
|
if c.Local == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.Local.Email
|
||||||
|
case ProviderOAuth:
|
||||||
|
if c.OAuth == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.OAuth.Email
|
||||||
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return base.Email
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) GetName() string {
|
func (c *UserContext) GetName() string {
|
||||||
base := c.getBaseContext()
|
switch c.Provider {
|
||||||
if base == nil {
|
case ProviderLocal:
|
||||||
|
if c.Local == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.Local.Name
|
||||||
|
case ProviderLDAP:
|
||||||
|
if c.LDAP == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.LDAP.Name
|
||||||
|
case ProviderBasicAuth:
|
||||||
|
if c.Local == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.Local.Name
|
||||||
|
case ProviderOAuth:
|
||||||
|
if c.OAuth == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.OAuth.Name
|
||||||
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return base.Name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) GetProviderID() string {
|
func (c *UserContext) GetProviderID() string {
|
||||||
@@ -226,8 +234,6 @@ func (c *UserContext) GetProviderID() string {
|
|||||||
return "ldap"
|
return "ldap"
|
||||||
case ProviderOAuth:
|
case ProviderOAuth:
|
||||||
return c.OAuth.ID
|
return c.OAuth.ID
|
||||||
case ProviderTailscale:
|
|
||||||
return "tailscale"
|
|
||||||
default:
|
default:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
@@ -246,10 +252,3 @@ func (c *UserContext) OAuthName() string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) TailscaleNodeName() string {
|
|
||||||
if c.Tailscale != nil {
|
|
||||||
return c.Tailscale.Username
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ type RuntimeConfig struct {
|
|||||||
OAuthWhitelist []string
|
OAuthWhitelist []string
|
||||||
ConfiguredProviders []Provider
|
ConfiguredProviders []Provider
|
||||||
OIDCClients []OIDCClientConfig
|
OIDCClients []OIDCClientConfig
|
||||||
TrustedDomains []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.31.1
|
// sqlc v1.30.0
|
||||||
|
|
||||||
package sqlite
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -1,472 +0,0 @@
|
|||||||
package memory_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ctx = context.Background()
|
|
||||||
|
|
||||||
func TestMemoryStore(t *testing.T) {
|
|
||||||
type testCase struct {
|
|
||||||
description string
|
|
||||||
run func(t *testing.T, s repository.Store)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []testCase{
|
|
||||||
{
|
|
||||||
description: "Create and get session",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
sess, err := s.CreateSession(ctx, repository.CreateSessionParams{
|
|
||||||
UUID: "uuid-1",
|
|
||||||
Username: "alice",
|
|
||||||
Expiry: 9999,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "uuid-1", sess.UUID)
|
|
||||||
assert.Equal(t, "alice", sess.Username)
|
|
||||||
|
|
||||||
got, err := s.GetSession(ctx, "uuid-1")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, sess, got)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Get session not found",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.GetSession(ctx, "missing")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Update session",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateSession(ctx, repository.CreateSessionParams{UUID: "uuid-1", Username: "alice"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
updated, err := s.UpdateSession(ctx, repository.UpdateSessionParams{
|
|
||||||
UUID: "uuid-1",
|
|
||||||
Username: "bob",
|
|
||||||
Email: "bob@example.com",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "bob", updated.Username)
|
|
||||||
assert.Equal(t, "bob@example.com", updated.Email)
|
|
||||||
|
|
||||||
got, err := s.GetSession(ctx, "uuid-1")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, updated, got)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Update session not found",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.UpdateSession(ctx, repository.UpdateSessionParams{UUID: "missing"})
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Delete session",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateSession(ctx, repository.CreateSessionParams{UUID: "uuid-1"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, s.DeleteSession(ctx, "uuid-1"))
|
|
||||||
|
|
||||||
_, err = s.GetSession(ctx, "uuid-1")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Delete expired sessions",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateSession(ctx, repository.CreateSessionParams{UUID: "expired", Expiry: 10})
|
|
||||||
require.NoError(t, err)
|
|
||||||
_, err = s.CreateSession(ctx, repository.CreateSessionParams{UUID: "valid", Expiry: 100})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, s.DeleteExpiredSessions(ctx, 50))
|
|
||||||
|
|
||||||
_, err = s.GetSession(ctx, "expired")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
|
|
||||||
_, err = s.GetSession(ctx, "valid")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Create and get OIDC code",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
code, err := s.CreateOidcCode(ctx, repository.CreateOidcCodeParams{
|
|
||||||
Sub: "sub-1",
|
|
||||||
CodeHash: "hash-1",
|
|
||||||
Scope: "openid",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "sub-1", code.Sub)
|
|
||||||
|
|
||||||
// destructive read removes the record
|
|
||||||
got, err := s.GetOidcCode(ctx, "hash-1")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, code, got)
|
|
||||||
|
|
||||||
_, err = s.GetOidcCode(ctx, "hash-1")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Get OIDC code not found",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.GetOidcCode(ctx, "missing")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Get OIDC code by sub",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateOidcCode(ctx, repository.CreateOidcCodeParams{Sub: "sub-1", CodeHash: "hash-1"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
got, err := s.GetOidcCodeBySub(ctx, "sub-1")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "sub-1", got.Sub)
|
|
||||||
|
|
||||||
// destructive — gone after read
|
|
||||||
_, err = s.GetOidcCodeBySub(ctx, "sub-1")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Get OIDC code by sub not found",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.GetOidcCodeBySub(ctx, "missing")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Get OIDC code unsafe",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateOidcCode(ctx, repository.CreateOidcCodeParams{Sub: "sub-1", CodeHash: "hash-1"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
got, err := s.GetOidcCodeUnsafe(ctx, "hash-1")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "sub-1", got.Sub)
|
|
||||||
|
|
||||||
// non-destructive — still present
|
|
||||||
_, err = s.GetOidcCodeUnsafe(ctx, "hash-1")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Get OIDC code unsafe not found",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.GetOidcCodeUnsafe(ctx, "missing")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Get OIDC code by sub unsafe",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateOidcCode(ctx, repository.CreateOidcCodeParams{Sub: "sub-1", CodeHash: "hash-1"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
got, err := s.GetOidcCodeBySubUnsafe(ctx, "sub-1")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "hash-1", got.CodeHash)
|
|
||||||
|
|
||||||
// non-destructive — still present
|
|
||||||
_, err = s.GetOidcCodeBySubUnsafe(ctx, "sub-1")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Get OIDC code by sub unsafe not found",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.GetOidcCodeBySubUnsafe(ctx, "missing")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Create OIDC code unique sub constraint",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateOidcCode(ctx, repository.CreateOidcCodeParams{Sub: "sub-1", CodeHash: "hash-1"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
_, err = s.CreateOidcCode(ctx, repository.CreateOidcCodeParams{Sub: "sub-1", CodeHash: "hash-2"})
|
|
||||||
assert.ErrorContains(t, err, "UNIQUE constraint failed: oidc_codes.sub")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Delete OIDC code",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateOidcCode(ctx, repository.CreateOidcCodeParams{Sub: "sub-1", CodeHash: "hash-1"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, s.DeleteOidcCode(ctx, "hash-1"))
|
|
||||||
|
|
||||||
_, err = s.GetOidcCodeUnsafe(ctx, "hash-1")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Delete OIDC code by sub",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateOidcCode(ctx, repository.CreateOidcCodeParams{Sub: "sub-1", CodeHash: "hash-1"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, s.DeleteOidcCodeBySub(ctx, "sub-1"))
|
|
||||||
|
|
||||||
_, err = s.GetOidcCodeUnsafe(ctx, "hash-1")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Delete expired OIDC codes",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateOidcCode(ctx, repository.CreateOidcCodeParams{Sub: "sub-1", CodeHash: "hash-1", ExpiresAt: 10})
|
|
||||||
require.NoError(t, err)
|
|
||||||
_, err = s.CreateOidcCode(ctx, repository.CreateOidcCodeParams{Sub: "sub-2", CodeHash: "hash-2", ExpiresAt: 100})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
deleted, err := s.DeleteExpiredOidcCodes(ctx, 50)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, deleted, 1)
|
|
||||||
assert.Equal(t, "hash-1", deleted[0].CodeHash)
|
|
||||||
|
|
||||||
_, err = s.GetOidcCodeUnsafe(ctx, "hash-2")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Create and get OIDC token",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
tok, err := s.CreateOidcToken(ctx, repository.CreateOidcTokenParams{
|
|
||||||
Sub: "sub-1",
|
|
||||||
AccessTokenHash: "at-hash-1",
|
|
||||||
CodeHash: "code-hash-1",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "sub-1", tok.Sub)
|
|
||||||
|
|
||||||
got, err := s.GetOidcToken(ctx, "at-hash-1")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tok, got)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Get OIDC token not found",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.GetOidcToken(ctx, "missing")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Create OIDC token unique sub constraint",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateOidcToken(ctx, repository.CreateOidcTokenParams{Sub: "sub-1", AccessTokenHash: "at-1"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
_, err = s.CreateOidcToken(ctx, repository.CreateOidcTokenParams{Sub: "sub-1", AccessTokenHash: "at-2"})
|
|
||||||
assert.ErrorContains(t, err, "UNIQUE constraint failed: oidc_tokens.sub")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Get OIDC token by refresh token",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateOidcToken(ctx, repository.CreateOidcTokenParams{
|
|
||||||
Sub: "sub-1",
|
|
||||||
AccessTokenHash: "at-1",
|
|
||||||
RefreshTokenHash: "rt-1",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
got, err := s.GetOidcTokenByRefreshToken(ctx, "rt-1")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "sub-1", got.Sub)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Get OIDC token by refresh token not found",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.GetOidcTokenByRefreshToken(ctx, "missing")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Get OIDC token by sub",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateOidcToken(ctx, repository.CreateOidcTokenParams{
|
|
||||||
Sub: "sub-1",
|
|
||||||
AccessTokenHash: "at-1",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
got, err := s.GetOidcTokenBySub(ctx, "sub-1")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "at-1", got.AccessTokenHash)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Get OIDC token by sub not found",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.GetOidcTokenBySub(ctx, "missing")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Update OIDC token by refresh token",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateOidcToken(ctx, repository.CreateOidcTokenParams{
|
|
||||||
Sub: "sub-1",
|
|
||||||
AccessTokenHash: "at-1",
|
|
||||||
RefreshTokenHash: "rt-1",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
updated, err := s.UpdateOidcTokenByRefreshToken(ctx, repository.UpdateOidcTokenByRefreshTokenParams{
|
|
||||||
RefreshTokenHash_2: "rt-1",
|
|
||||||
AccessTokenHash: "at-2",
|
|
||||||
RefreshTokenHash: "rt-2",
|
|
||||||
TokenExpiresAt: 200,
|
|
||||||
RefreshTokenExpiresAt: 400,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "at-2", updated.AccessTokenHash)
|
|
||||||
assert.Equal(t, "rt-2", updated.RefreshTokenHash)
|
|
||||||
|
|
||||||
// old key gone, new key present
|
|
||||||
_, err = s.GetOidcToken(ctx, "at-1")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
|
|
||||||
got, err := s.GetOidcToken(ctx, "at-2")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "sub-1", got.Sub)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Update OIDC token by refresh token not found",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.UpdateOidcTokenByRefreshToken(ctx, repository.UpdateOidcTokenByRefreshTokenParams{
|
|
||||||
RefreshTokenHash_2: "missing",
|
|
||||||
})
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Delete OIDC token",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateOidcToken(ctx, repository.CreateOidcTokenParams{Sub: "sub-1", AccessTokenHash: "at-1"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, s.DeleteOidcToken(ctx, "at-1"))
|
|
||||||
|
|
||||||
_, err = s.GetOidcToken(ctx, "at-1")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Delete OIDC token by sub",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateOidcToken(ctx, repository.CreateOidcTokenParams{Sub: "sub-1", AccessTokenHash: "at-1"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, s.DeleteOidcTokenBySub(ctx, "sub-1"))
|
|
||||||
|
|
||||||
_, err = s.GetOidcToken(ctx, "at-1")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Delete OIDC token by code hash",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateOidcToken(ctx, repository.CreateOidcTokenParams{
|
|
||||||
Sub: "sub-1",
|
|
||||||
AccessTokenHash: "at-1",
|
|
||||||
CodeHash: "code-1",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, s.DeleteOidcTokenByCodeHash(ctx, "code-1"))
|
|
||||||
|
|
||||||
_, err = s.GetOidcToken(ctx, "at-1")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Delete expired OIDC tokens",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
// both expiries past
|
|
||||||
_, err := s.CreateOidcToken(ctx, repository.CreateOidcTokenParams{
|
|
||||||
Sub: "sub-1", AccessTokenHash: "at-1",
|
|
||||||
TokenExpiresAt: 10, RefreshTokenExpiresAt: 10,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
// valid
|
|
||||||
_, err = s.CreateOidcToken(ctx, repository.CreateOidcTokenParams{
|
|
||||||
Sub: "sub-3", AccessTokenHash: "at-3",
|
|
||||||
TokenExpiresAt: 100, RefreshTokenExpiresAt: 100,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
deleted, err := s.DeleteExpiredOidcTokens(ctx, repository.DeleteExpiredOidcTokensParams{
|
|
||||||
TokenExpiresAt: 50,
|
|
||||||
RefreshTokenExpiresAt: 50,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Len(t, deleted, 1)
|
|
||||||
|
|
||||||
_, err = s.GetOidcToken(ctx, "at-3")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Create and get OIDC user info",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
u, err := s.CreateOidcUserInfo(ctx, repository.CreateOidcUserInfoParams{
|
|
||||||
Sub: "sub-1",
|
|
||||||
Name: "Alice",
|
|
||||||
Email: "alice@example.com",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "sub-1", u.Sub)
|
|
||||||
|
|
||||||
got, err := s.GetOidcUserInfo(ctx, "sub-1")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, u, got)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Get OIDC user info not found",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.GetOidcUserInfo(ctx, "missing")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Delete OIDC user info",
|
|
||||||
run: func(t *testing.T, s repository.Store) {
|
|
||||||
_, err := s.CreateOidcUserInfo(ctx, repository.CreateOidcUserInfoParams{Sub: "sub-1"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, s.DeleteOidcUserInfo(ctx, "sub-1"))
|
|
||||||
|
|
||||||
_, err = s.GetOidcUserInfo(ctx, "sub-1")
|
|
||||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.description, func(t *testing.T) {
|
|
||||||
s := memory.New()
|
|
||||||
test.run(t, s)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
package memory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Store) CreateOidcCode(_ context.Context, arg repository.CreateOidcCodeParams) (repository.OidcCode, error) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
// Enforce sub UNIQUE constraint
|
|
||||||
for _, c := range s.oidcCodes {
|
|
||||||
if c.Sub == arg.Sub {
|
|
||||||
return repository.OidcCode{}, fmt.Errorf("UNIQUE constraint failed: oidc_codes.sub")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
code := repository.OidcCode(arg)
|
|
||||||
s.oidcCodes[arg.CodeHash] = code
|
|
||||||
return code, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOidcCode is a destructive read: it deletes and returns the code (mirrors SQLite's DELETE...RETURNING).
|
|
||||||
func (s *Store) GetOidcCode(_ context.Context, codeHash string) (repository.OidcCode, error) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
c, ok := s.oidcCodes[codeHash]
|
|
||||||
if !ok {
|
|
||||||
return repository.OidcCode{}, repository.ErrNotFound
|
|
||||||
}
|
|
||||||
delete(s.oidcCodes, codeHash)
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOidcCodeBySub is a destructive read: it deletes and returns the code (mirrors SQLite's DELETE...RETURNING).
|
|
||||||
func (s *Store) GetOidcCodeBySub(_ context.Context, sub string) (repository.OidcCode, error) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
for k, c := range s.oidcCodes {
|
|
||||||
if c.Sub == sub {
|
|
||||||
delete(s.oidcCodes, k)
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return repository.OidcCode{}, repository.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOidcCodeUnsafe is a non-destructive read (mirrors SQLite's SELECT).
|
|
||||||
func (s *Store) GetOidcCodeUnsafe(_ context.Context, codeHash string) (repository.OidcCode, error) {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
c, ok := s.oidcCodes[codeHash]
|
|
||||||
if !ok {
|
|
||||||
return repository.OidcCode{}, repository.ErrNotFound
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOidcCodeBySubUnsafe is a non-destructive read (mirrors SQLite's SELECT).
|
|
||||||
func (s *Store) GetOidcCodeBySubUnsafe(_ context.Context, sub string) (repository.OidcCode, error) {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
for _, c := range s.oidcCodes {
|
|
||||||
if c.Sub == sub {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return repository.OidcCode{}, repository.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteOidcCode(_ context.Context, codeHash string) error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
delete(s.oidcCodes, codeHash)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteOidcCodeBySub(_ context.Context, sub string) error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
for k, c := range s.oidcCodes {
|
|
||||||
if c.Sub == sub {
|
|
||||||
delete(s.oidcCodes, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteExpiredOidcCodes(_ context.Context, expiresAt int64) ([]repository.OidcCode, error) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
var deleted []repository.OidcCode
|
|
||||||
for k, c := range s.oidcCodes {
|
|
||||||
if c.ExpiresAt < expiresAt {
|
|
||||||
deleted = append(deleted, c)
|
|
||||||
delete(s.oidcCodes, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return deleted, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CreateOidcToken(_ context.Context, arg repository.CreateOidcTokenParams) (repository.OidcToken, error) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
// Enforce sub UNIQUE constraint
|
|
||||||
for _, t := range s.oidcTokens {
|
|
||||||
if t.Sub == arg.Sub {
|
|
||||||
return repository.OidcToken{}, fmt.Errorf("UNIQUE constraint failed: oidc_tokens.sub")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tok := repository.OidcToken{
|
|
||||||
Sub: arg.Sub,
|
|
||||||
AccessTokenHash: arg.AccessTokenHash,
|
|
||||||
RefreshTokenHash: arg.RefreshTokenHash,
|
|
||||||
CodeHash: arg.CodeHash,
|
|
||||||
Scope: arg.Scope,
|
|
||||||
ClientID: arg.ClientID,
|
|
||||||
TokenExpiresAt: arg.TokenExpiresAt,
|
|
||||||
RefreshTokenExpiresAt: arg.RefreshTokenExpiresAt,
|
|
||||||
Nonce: arg.Nonce,
|
|
||||||
}
|
|
||||||
s.oidcTokens[arg.AccessTokenHash] = tok
|
|
||||||
return tok, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetOidcToken(_ context.Context, accessTokenHash string) (repository.OidcToken, error) {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
t, ok := s.oidcTokens[accessTokenHash]
|
|
||||||
if !ok {
|
|
||||||
return repository.OidcToken{}, repository.ErrNotFound
|
|
||||||
}
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetOidcTokenByRefreshToken(_ context.Context, refreshTokenHash string) (repository.OidcToken, error) {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
for _, t := range s.oidcTokens {
|
|
||||||
if t.RefreshTokenHash == refreshTokenHash {
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return repository.OidcToken{}, repository.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetOidcTokenBySub(_ context.Context, sub string) (repository.OidcToken, error) {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
for _, t := range s.oidcTokens {
|
|
||||||
if t.Sub == sub {
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return repository.OidcToken{}, repository.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateOidcTokenByRefreshToken(_ context.Context, arg repository.UpdateOidcTokenByRefreshTokenParams) (repository.OidcToken, error) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
for k, t := range s.oidcTokens {
|
|
||||||
if t.RefreshTokenHash == arg.RefreshTokenHash_2 {
|
|
||||||
delete(s.oidcTokens, k)
|
|
||||||
t.AccessTokenHash = arg.AccessTokenHash
|
|
||||||
t.RefreshTokenHash = arg.RefreshTokenHash
|
|
||||||
t.TokenExpiresAt = arg.TokenExpiresAt
|
|
||||||
t.RefreshTokenExpiresAt = arg.RefreshTokenExpiresAt
|
|
||||||
s.oidcTokens[arg.AccessTokenHash] = t
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return repository.OidcToken{}, repository.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteOidcToken(_ context.Context, accessTokenHash string) error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
delete(s.oidcTokens, accessTokenHash)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteOidcTokenBySub(_ context.Context, sub string) error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
for k, t := range s.oidcTokens {
|
|
||||||
if t.Sub == sub {
|
|
||||||
delete(s.oidcTokens, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteOidcTokenByCodeHash(_ context.Context, codeHash string) error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
for k, t := range s.oidcTokens {
|
|
||||||
if t.CodeHash == codeHash {
|
|
||||||
delete(s.oidcTokens, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteExpiredOidcTokens(_ context.Context, arg repository.DeleteExpiredOidcTokensParams) ([]repository.OidcToken, error) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
var deleted []repository.OidcToken
|
|
||||||
for k, t := range s.oidcTokens {
|
|
||||||
if t.TokenExpiresAt < arg.TokenExpiresAt && t.RefreshTokenExpiresAt < arg.RefreshTokenExpiresAt {
|
|
||||||
deleted = append(deleted, t)
|
|
||||||
delete(s.oidcTokens, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return deleted, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CreateOidcUserInfo(_ context.Context, arg repository.CreateOidcUserInfoParams) (repository.OidcUserinfo, error) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
u := repository.OidcUserinfo(arg)
|
|
||||||
s.oidcUsers[arg.Sub] = u
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetOidcUserInfo(_ context.Context, sub string) (repository.OidcUserinfo, error) {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
u, ok := s.oidcUsers[sub]
|
|
||||||
if !ok {
|
|
||||||
return repository.OidcUserinfo{}, repository.ErrNotFound
|
|
||||||
}
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteOidcUserInfo(_ context.Context, sub string) error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
delete(s.oidcUsers, sub)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package memory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Store) CreateSession(_ context.Context, arg repository.CreateSessionParams) (repository.Session, error) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
sess := repository.Session(arg)
|
|
||||||
s.sessions[arg.UUID] = sess
|
|
||||||
return sess, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetSession(_ context.Context, uuid string) (repository.Session, error) {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
sess, ok := s.sessions[uuid]
|
|
||||||
if !ok {
|
|
||||||
return repository.Session{}, repository.ErrNotFound
|
|
||||||
}
|
|
||||||
return sess, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateSession(_ context.Context, arg repository.UpdateSessionParams) (repository.Session, error) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
sess, ok := s.sessions[arg.UUID]
|
|
||||||
if !ok {
|
|
||||||
return repository.Session{}, repository.ErrNotFound
|
|
||||||
}
|
|
||||||
sess.Username = arg.Username
|
|
||||||
sess.Email = arg.Email
|
|
||||||
sess.Name = arg.Name
|
|
||||||
sess.Provider = arg.Provider
|
|
||||||
sess.TotpPending = arg.TotpPending
|
|
||||||
sess.OAuthGroups = arg.OAuthGroups
|
|
||||||
sess.Expiry = arg.Expiry
|
|
||||||
sess.OAuthName = arg.OAuthName
|
|
||||||
sess.OAuthSub = arg.OAuthSub
|
|
||||||
s.sessions[arg.UUID] = sess
|
|
||||||
return sess, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteSession(_ context.Context, uuid string) error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
delete(s.sessions, uuid)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteExpiredSessions(_ context.Context, expiry int64) error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
for k, v := range s.sessions {
|
|
||||||
if v.Expiry < expiry {
|
|
||||||
delete(s.sessions, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// Package memory provides an in-memory implementation of repository.Store for use in tests.
|
|
||||||
package memory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Store is a thread-safe in-memory implementation of repository.Store.
|
|
||||||
type Store struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
sessions map[string]repository.Session
|
|
||||||
oidcCodes map[string]repository.OidcCode
|
|
||||||
oidcTokens map[string]repository.OidcToken
|
|
||||||
oidcUsers map[string]repository.OidcUserinfo
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a new empty in-memory Store.
|
|
||||||
func New() repository.Store {
|
|
||||||
return &Store{
|
|
||||||
sessions: make(map[string]repository.Session),
|
|
||||||
oidcCodes: make(map[string]repository.OidcCode),
|
|
||||||
oidcTokens: make(map[string]repository.OidcToken),
|
|
||||||
oidcUsers: make(map[string]repository.OidcUserinfo),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,9 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
// Shared model and parameter types for all storage drivers.
|
|
||||||
// sqlc-generated driver packages use these via the conversion layer in their store.go.
|
|
||||||
|
|
||||||
type Session struct {
|
|
||||||
UUID string
|
|
||||||
Username string
|
|
||||||
Email string
|
|
||||||
Name string
|
|
||||||
Provider string
|
|
||||||
TotpPending bool
|
|
||||||
OAuthGroups string
|
|
||||||
Expiry int64
|
|
||||||
CreatedAt int64
|
|
||||||
OAuthName string
|
|
||||||
OAuthSub string
|
|
||||||
}
|
|
||||||
|
|
||||||
type OidcCode struct {
|
type OidcCode struct {
|
||||||
Sub string
|
Sub string
|
||||||
CodeHash string
|
CodeHash string
|
||||||
@@ -62,7 +49,7 @@ type OidcUserinfo struct {
|
|||||||
Address string
|
Address string
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateSessionParams struct {
|
type Session struct {
|
||||||
UUID string
|
UUID string
|
||||||
Username string
|
Username string
|
||||||
Email string
|
Email string
|
||||||
@@ -75,74 +62,3 @@ type CreateSessionParams struct {
|
|||||||
OAuthName string
|
OAuthName string
|
||||||
OAuthSub string
|
OAuthSub string
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateSessionParams struct {
|
|
||||||
Username string
|
|
||||||
Email string
|
|
||||||
Name string
|
|
||||||
Provider string
|
|
||||||
TotpPending bool
|
|
||||||
OAuthGroups string
|
|
||||||
Expiry int64
|
|
||||||
OAuthName string
|
|
||||||
OAuthSub string
|
|
||||||
UUID string
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateOidcCodeParams struct {
|
|
||||||
Sub string
|
|
||||||
CodeHash string
|
|
||||||
Scope string
|
|
||||||
RedirectURI string
|
|
||||||
ClientID string
|
|
||||||
ExpiresAt int64
|
|
||||||
Nonce string
|
|
||||||
CodeChallenge string
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateOidcTokenParams struct {
|
|
||||||
Sub string
|
|
||||||
AccessTokenHash string
|
|
||||||
RefreshTokenHash string
|
|
||||||
Scope string
|
|
||||||
ClientID string
|
|
||||||
TokenExpiresAt int64
|
|
||||||
RefreshTokenExpiresAt int64
|
|
||||||
CodeHash string
|
|
||||||
Nonce string
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateOidcTokenByRefreshTokenParams struct {
|
|
||||||
AccessTokenHash string
|
|
||||||
RefreshTokenHash string
|
|
||||||
TokenExpiresAt int64
|
|
||||||
RefreshTokenExpiresAt int64
|
|
||||||
RefreshTokenHash_2 string
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeleteExpiredOidcTokensParams struct {
|
|
||||||
TokenExpiresAt int64
|
|
||||||
RefreshTokenExpiresAt int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateOidcUserInfoParams struct {
|
|
||||||
Sub string
|
|
||||||
Name string
|
|
||||||
PreferredUsername string
|
|
||||||
Email string
|
|
||||||
Groups string
|
|
||||||
UpdatedAt int64
|
|
||||||
GivenName string
|
|
||||||
FamilyName string
|
|
||||||
MiddleName string
|
|
||||||
Nickname string
|
|
||||||
Profile string
|
|
||||||
Picture string
|
|
||||||
Website string
|
|
||||||
Gender string
|
|
||||||
Birthdate string
|
|
||||||
Zoneinfo string
|
|
||||||
Locale string
|
|
||||||
PhoneNumber string
|
|
||||||
Address string
|
|
||||||
}
|
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.31.1
|
// sqlc v1.30.0
|
||||||
// source: oidc_queries.sql
|
// source: oidc_queries.sql
|
||||||
|
|
||||||
package sqlite
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.31.1
|
// sqlc v1.30.0
|
||||||
// source: session_queries.sql
|
// source: session_queries.sql
|
||||||
|
|
||||||
package sqlite
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package sqlite
|
|
||||||
|
|
||||||
//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc-wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/sqlite
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
|
|
||||||
package sqlite
|
|
||||||
|
|
||||||
type OidcCode struct {
|
|
||||||
Sub string
|
|
||||||
CodeHash string
|
|
||||||
Scope string
|
|
||||||
RedirectURI string
|
|
||||||
ClientID string
|
|
||||||
ExpiresAt int64
|
|
||||||
Nonce string
|
|
||||||
CodeChallenge string
|
|
||||||
}
|
|
||||||
|
|
||||||
type OidcToken struct {
|
|
||||||
Sub string
|
|
||||||
AccessTokenHash string
|
|
||||||
RefreshTokenHash string
|
|
||||||
CodeHash string
|
|
||||||
Scope string
|
|
||||||
ClientID string
|
|
||||||
TokenExpiresAt int64
|
|
||||||
RefreshTokenExpiresAt int64
|
|
||||||
Nonce string
|
|
||||||
}
|
|
||||||
|
|
||||||
type OidcUserinfo struct {
|
|
||||||
Sub string
|
|
||||||
Name string
|
|
||||||
PreferredUsername string
|
|
||||||
Email string
|
|
||||||
Groups string
|
|
||||||
UpdatedAt int64
|
|
||||||
GivenName string
|
|
||||||
FamilyName string
|
|
||||||
MiddleName string
|
|
||||||
Nickname string
|
|
||||||
Profile string
|
|
||||||
Picture string
|
|
||||||
Website string
|
|
||||||
Gender string
|
|
||||||
Birthdate string
|
|
||||||
Zoneinfo string
|
|
||||||
Locale string
|
|
||||||
PhoneNumber string
|
|
||||||
Address string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Session struct {
|
|
||||||
UUID string
|
|
||||||
Username string
|
|
||||||
Email string
|
|
||||||
Name string
|
|
||||||
Provider string
|
|
||||||
TotpPending bool
|
|
||||||
OAuthGroups string
|
|
||||||
Expiry int64
|
|
||||||
CreatedAt int64
|
|
||||||
OAuthName string
|
|
||||||
OAuthSub string
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
// Code generated by cmd/gen/sqlc-wrapper. DO NOT EDIT.
|
|
||||||
package sqlite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Store wraps *Queries and implements repository.Store.
|
|
||||||
type Store struct {
|
|
||||||
q *Queries
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStore wraps a *Queries to satisfy repository.Store.
|
|
||||||
func NewStore(q *Queries) repository.Store {
|
|
||||||
return &Store{q: q}
|
|
||||||
}
|
|
||||||
|
|
||||||
var errorMap = map[error]error{
|
|
||||||
sql.ErrNoRows: repository.ErrNotFound,
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapErr(err error) error {
|
|
||||||
for from, to := range errorMap {
|
|
||||||
if errors.Is(err, from) {
|
|
||||||
return to
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CreateOidcCode(ctx context.Context, arg repository.CreateOidcCodeParams) (repository.OidcCode, error) {
|
|
||||||
r, err := s.q.CreateOidcCode(ctx, CreateOidcCodeParams(arg))
|
|
||||||
if err != nil {
|
|
||||||
return repository.OidcCode{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return repository.OidcCode(r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CreateOidcToken(ctx context.Context, arg repository.CreateOidcTokenParams) (repository.OidcToken, error) {
|
|
||||||
r, err := s.q.CreateOidcToken(ctx, CreateOidcTokenParams(arg))
|
|
||||||
if err != nil {
|
|
||||||
return repository.OidcToken{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return repository.OidcToken(r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CreateOidcUserInfo(ctx context.Context, arg repository.CreateOidcUserInfoParams) (repository.OidcUserinfo, error) {
|
|
||||||
r, err := s.q.CreateOidcUserInfo(ctx, CreateOidcUserInfoParams(arg))
|
|
||||||
if err != nil {
|
|
||||||
return repository.OidcUserinfo{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return repository.OidcUserinfo(r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CreateSession(ctx context.Context, arg repository.CreateSessionParams) (repository.Session, error) {
|
|
||||||
r, err := s.q.CreateSession(ctx, CreateSessionParams(arg))
|
|
||||||
if err != nil {
|
|
||||||
return repository.Session{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return repository.Session(r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) ([]repository.OidcCode, error) {
|
|
||||||
rows, err := s.q.DeleteExpiredOidcCodes(ctx, expiresAt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, mapErr(err)
|
|
||||||
}
|
|
||||||
out := make([]repository.OidcCode, len(rows))
|
|
||||||
for i, row := range rows {
|
|
||||||
out[i] = repository.OidcCode(row)
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteExpiredOidcTokens(ctx context.Context, arg repository.DeleteExpiredOidcTokensParams) ([]repository.OidcToken, error) {
|
|
||||||
rows, err := s.q.DeleteExpiredOidcTokens(ctx, DeleteExpiredOidcTokensParams(arg))
|
|
||||||
if err != nil {
|
|
||||||
return nil, mapErr(err)
|
|
||||||
}
|
|
||||||
out := make([]repository.OidcToken, len(rows))
|
|
||||||
for i, row := range rows {
|
|
||||||
out[i] = repository.OidcToken(row)
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteExpiredSessions(ctx context.Context, expiry int64) error {
|
|
||||||
return mapErr(s.q.DeleteExpiredSessions(ctx, expiry))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteOidcCode(ctx context.Context, codeHash string) error {
|
|
||||||
return mapErr(s.q.DeleteOidcCode(ctx, codeHash))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteOidcCodeBySub(ctx context.Context, sub string) error {
|
|
||||||
return mapErr(s.q.DeleteOidcCodeBySub(ctx, sub))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteOidcToken(ctx context.Context, accessTokenHash string) error {
|
|
||||||
return mapErr(s.q.DeleteOidcToken(ctx, accessTokenHash))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteOidcTokenByCodeHash(ctx context.Context, codeHash string) error {
|
|
||||||
return mapErr(s.q.DeleteOidcTokenByCodeHash(ctx, codeHash))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteOidcTokenBySub(ctx context.Context, sub string) error {
|
|
||||||
return mapErr(s.q.DeleteOidcTokenBySub(ctx, sub))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteOidcUserInfo(ctx context.Context, sub string) error {
|
|
||||||
return mapErr(s.q.DeleteOidcUserInfo(ctx, sub))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteSession(ctx context.Context, uuid string) error {
|
|
||||||
return mapErr(s.q.DeleteSession(ctx, uuid))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetOidcCode(ctx context.Context, codeHash string) (repository.OidcCode, error) {
|
|
||||||
r, err := s.q.GetOidcCode(ctx, codeHash)
|
|
||||||
if err != nil {
|
|
||||||
return repository.OidcCode{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return repository.OidcCode(r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetOidcCodeBySub(ctx context.Context, sub string) (repository.OidcCode, error) {
|
|
||||||
r, err := s.q.GetOidcCodeBySub(ctx, sub)
|
|
||||||
if err != nil {
|
|
||||||
return repository.OidcCode{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return repository.OidcCode(r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetOidcCodeBySubUnsafe(ctx context.Context, sub string) (repository.OidcCode, error) {
|
|
||||||
r, err := s.q.GetOidcCodeBySubUnsafe(ctx, sub)
|
|
||||||
if err != nil {
|
|
||||||
return repository.OidcCode{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return repository.OidcCode(r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetOidcCodeUnsafe(ctx context.Context, codeHash string) (repository.OidcCode, error) {
|
|
||||||
r, err := s.q.GetOidcCodeUnsafe(ctx, codeHash)
|
|
||||||
if err != nil {
|
|
||||||
return repository.OidcCode{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return repository.OidcCode(r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetOidcToken(ctx context.Context, accessTokenHash string) (repository.OidcToken, error) {
|
|
||||||
r, err := s.q.GetOidcToken(ctx, accessTokenHash)
|
|
||||||
if err != nil {
|
|
||||||
return repository.OidcToken{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return repository.OidcToken(r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetOidcTokenByRefreshToken(ctx context.Context, refreshTokenHash string) (repository.OidcToken, error) {
|
|
||||||
r, err := s.q.GetOidcTokenByRefreshToken(ctx, refreshTokenHash)
|
|
||||||
if err != nil {
|
|
||||||
return repository.OidcToken{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return repository.OidcToken(r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetOidcTokenBySub(ctx context.Context, sub string) (repository.OidcToken, error) {
|
|
||||||
r, err := s.q.GetOidcTokenBySub(ctx, sub)
|
|
||||||
if err != nil {
|
|
||||||
return repository.OidcToken{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return repository.OidcToken(r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetOidcUserInfo(ctx context.Context, sub string) (repository.OidcUserinfo, error) {
|
|
||||||
r, err := s.q.GetOidcUserInfo(ctx, sub)
|
|
||||||
if err != nil {
|
|
||||||
return repository.OidcUserinfo{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return repository.OidcUserinfo(r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetSession(ctx context.Context, uuid string) (repository.Session, error) {
|
|
||||||
r, err := s.q.GetSession(ctx, uuid)
|
|
||||||
if err != nil {
|
|
||||||
return repository.Session{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return repository.Session(r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateOidcTokenByRefreshToken(ctx context.Context, arg repository.UpdateOidcTokenByRefreshTokenParams) (repository.OidcToken, error) {
|
|
||||||
r, err := s.q.UpdateOidcTokenByRefreshToken(ctx, UpdateOidcTokenByRefreshTokenParams(arg))
|
|
||||||
if err != nil {
|
|
||||||
return repository.OidcToken{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return repository.OidcToken(r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateSession(ctx context.Context, arg repository.UpdateSessionParams) (repository.Session, error) {
|
|
||||||
r, err := s.q.UpdateSession(ctx, UpdateSessionParams(arg))
|
|
||||||
if err != nil {
|
|
||||||
return repository.Session{}, mapErr(err)
|
|
||||||
}
|
|
||||||
return repository.Session(r), nil
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrNotFound is returned by Store methods when the requested record does not exist.
|
|
||||||
var ErrNotFound = errors.New("not found")
|
|
||||||
|
|
||||||
// Store is the interface that all storage drivers must implement.
|
|
||||||
// The sqlc-generated *Queries struct satisfies this interface for SQLite.
|
|
||||||
// Future drivers (postgres, etc.) must return the shared types defined in this package.
|
|
||||||
type Store interface {
|
|
||||||
// Sessions
|
|
||||||
CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
|
|
||||||
GetSession(ctx context.Context, uuid string) (Session, error)
|
|
||||||
UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error)
|
|
||||||
DeleteSession(ctx context.Context, uuid string) error
|
|
||||||
DeleteExpiredSessions(ctx context.Context, expiry int64) error
|
|
||||||
|
|
||||||
// OIDC codes
|
|
||||||
CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams) (OidcCode, error)
|
|
||||||
GetOidcCode(ctx context.Context, codeHash string) (OidcCode, error)
|
|
||||||
GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, error)
|
|
||||||
GetOidcCodeUnsafe(ctx context.Context, codeHash string) (OidcCode, error)
|
|
||||||
GetOidcCodeBySubUnsafe(ctx context.Context, sub string) (OidcCode, error)
|
|
||||||
DeleteOidcCode(ctx context.Context, codeHash string) error
|
|
||||||
DeleteOidcCodeBySub(ctx context.Context, sub string) error
|
|
||||||
DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) ([]OidcCode, error)
|
|
||||||
|
|
||||||
// OIDC tokens
|
|
||||||
CreateOidcToken(ctx context.Context, arg CreateOidcTokenParams) (OidcToken, error)
|
|
||||||
GetOidcToken(ctx context.Context, accessTokenHash string) (OidcToken, error)
|
|
||||||
GetOidcTokenByRefreshToken(ctx context.Context, refreshTokenHash string) (OidcToken, error)
|
|
||||||
GetOidcTokenBySub(ctx context.Context, sub string) (OidcToken, error)
|
|
||||||
UpdateOidcTokenByRefreshToken(ctx context.Context, arg UpdateOidcTokenByRefreshTokenParams) (OidcToken, error)
|
|
||||||
DeleteOidcToken(ctx context.Context, accessTokenHash string) error
|
|
||||||
DeleteOidcTokenBySub(ctx context.Context, sub string) error
|
|
||||||
DeleteOidcTokenByCodeHash(ctx context.Context, codeHash string) error
|
|
||||||
DeleteExpiredOidcTokens(ctx context.Context, arg DeleteExpiredOidcTokensParams) ([]OidcToken, error)
|
|
||||||
|
|
||||||
// OIDC userinfo
|
|
||||||
CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfoParams) (OidcUserinfo, error)
|
|
||||||
GetOidcUserInfo(ctx context.Context, sub string) (OidcUserinfo, error)
|
|
||||||
DeleteOidcUserInfo(ctx context.Context, sub string) error
|
|
||||||
}
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RuleName string
|
|
||||||
|
|
||||||
const (
|
|
||||||
RuleUserAllowed RuleName = "rule-user-allowed"
|
|
||||||
RuleOAuthGroup RuleName = "rule-oauth-group"
|
|
||||||
RuleLDAPGroup RuleName = "rule-ldap-group"
|
|
||||||
RuleAuthEnabled RuleName = "rule-auth-enabled"
|
|
||||||
RuleIPAllowed RuleName = "rule-ip-allowed"
|
|
||||||
RuleIPBypassed RuleName = "rule-ip-bypassed"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserAllowedRule struct {
|
|
||||||
Log *logger.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rule *UserAllowedRule) Evaluate(ctx *ACLContext) Effect {
|
|
||||||
if ctx.ACLs == nil || ctx.UserContext == nil {
|
|
||||||
return EffectAbstain
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.UserContext.Provider == model.ProviderOAuth {
|
|
||||||
rule.Log.App.Debug().Msg("User is an OAuth user, checking OAuth whitelist")
|
|
||||||
match, err := utils.CheckFilter(ctx.ACLs.OAuth.Whitelist, ctx.UserContext.OAuth.Email)
|
|
||||||
if err != nil {
|
|
||||||
rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.OAuth.Email).Msg("Invalid entry in OAuth whitelist")
|
|
||||||
return EffectAbstain
|
|
||||||
}
|
|
||||||
if match {
|
|
||||||
rule.Log.App.Debug().Str("email", ctx.UserContext.OAuth.Email).Msg("User is in OAuth whitelist, allowing access")
|
|
||||||
return EffectAllow
|
|
||||||
}
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.ACLs.Users.Block != "" {
|
|
||||||
rule.Log.App.Debug().Msg("Checking users block list")
|
|
||||||
match, err := utils.CheckFilter(ctx.ACLs.Users.Block, ctx.UserContext.GetUsername())
|
|
||||||
if err != nil {
|
|
||||||
rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.GetUsername()).Msg("Invalid entry in users block list")
|
|
||||||
return EffectAbstain
|
|
||||||
}
|
|
||||||
if match {
|
|
||||||
rule.Log.App.Debug().Str("username", ctx.UserContext.GetUsername()).Msg("User is in users block list, denying access")
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
return EffectAllow
|
|
||||||
}
|
|
||||||
|
|
||||||
rule.Log.App.Debug().Msg("Checking users allow list")
|
|
||||||
|
|
||||||
match, err := utils.CheckFilter(ctx.ACLs.Users.Allow, ctx.UserContext.GetUsername())
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.GetUsername()).Msg("Invalid entry in users allow list")
|
|
||||||
return EffectAbstain
|
|
||||||
}
|
|
||||||
|
|
||||||
if match {
|
|
||||||
rule.Log.App.Debug().Str("username", ctx.UserContext.GetUsername()).Msg("User is in users allow list, allowing access")
|
|
||||||
return EffectAllow
|
|
||||||
}
|
|
||||||
|
|
||||||
rule.Log.App.Debug().Str("username", ctx.UserContext.GetUsername()).Msg("User is not in users allow list, denying access")
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
|
|
||||||
type OAuthGroupRule struct {
|
|
||||||
Log *logger.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rule *OAuthGroupRule) Evaluate(ctx *ACLContext) Effect {
|
|
||||||
if ctx.ACLs == nil || ctx.UserContext == nil {
|
|
||||||
return EffectAbstain
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ctx.UserContext.IsOAuth() {
|
|
||||||
rule.Log.App.Debug().Msg("User is not an OAuth user, skipping OAuth group check")
|
|
||||||
return EffectAbstain
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := model.OverrideProviders[ctx.UserContext.OAuth.ID]; ok {
|
|
||||||
rule.Log.App.Debug().Str("provider", ctx.UserContext.OAuth.ID).Msg("Provider override detected, skipping group check")
|
|
||||||
return EffectAllow
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, group := range ctx.UserContext.OAuth.Groups {
|
|
||||||
match, err := utils.CheckFilter(ctx.ACLs.OAuth.Groups, strings.TrimSpace(group))
|
|
||||||
if err != nil {
|
|
||||||
return EffectAbstain
|
|
||||||
}
|
|
||||||
if match {
|
|
||||||
rule.Log.App.Trace().Str("group", group).Str("required", ctx.ACLs.OAuth.Groups).Msg("User group matched, allowing access")
|
|
||||||
return EffectAllow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rule.Log.App.Debug().Msg("No groups matched")
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
|
|
||||||
type LDAPGroupRule struct {
|
|
||||||
Log *logger.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rule *LDAPGroupRule) Evaluate(ctx *ACLContext) Effect {
|
|
||||||
if ctx == nil || ctx.UserContext == nil {
|
|
||||||
return EffectAbstain
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ctx.UserContext.IsLDAP() {
|
|
||||||
rule.Log.App.Debug().Msg("User is not an LDAP user, skipping LDAP group check")
|
|
||||||
return EffectAbstain
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, group := range ctx.UserContext.LDAP.Groups {
|
|
||||||
match, err := utils.CheckFilter(ctx.ACLs.LDAP.Groups, strings.TrimSpace(group))
|
|
||||||
if err != nil {
|
|
||||||
return EffectAbstain
|
|
||||||
}
|
|
||||||
if match {
|
|
||||||
rule.Log.App.Trace().Str("group", group).Str("required", ctx.ACLs.LDAP.Groups).Msg("User group matched, allowing access")
|
|
||||||
return EffectAllow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rule.Log.App.Debug().Msg("No groups matched")
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthEnabledRule struct {
|
|
||||||
Log *logger.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rule *AuthEnabledRule) Evaluate(ctx *ACLContext) Effect {
|
|
||||||
if ctx.ACLs == nil {
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.ACLs.Path.Block != "" {
|
|
||||||
regex, err := regexp.Compile(ctx.ACLs.Path.Block)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
rule.Log.App.Error().Err(err).Msg("Failed to compile block regex")
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
|
|
||||||
if !regex.MatchString(ctx.Path) {
|
|
||||||
return EffectAllow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.ACLs.Path.Allow != "" {
|
|
||||||
regex, err := regexp.Compile(ctx.ACLs.Path.Allow)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
rule.Log.App.Error().Err(err).Msg("Failed to compile allow regex")
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
|
|
||||||
if regex.MatchString(ctx.Path) {
|
|
||||||
return EffectAllow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
|
|
||||||
type IPAllowedRule struct {
|
|
||||||
Log *logger.Logger
|
|
||||||
Config model.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rule *IPAllowedRule) Evaluate(ctx *ACLContext) Effect {
|
|
||||||
if ctx.ACLs == nil {
|
|
||||||
return EffectAbstain
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge the global and app IP filter
|
|
||||||
blockedIps := append(ctx.ACLs.IP.Block, rule.Config.Auth.IP.Block...)
|
|
||||||
allowedIPs := append(ctx.ACLs.IP.Allow, rule.Config.Auth.IP.Allow...)
|
|
||||||
|
|
||||||
for _, blocked := range blockedIps {
|
|
||||||
match, err := utils.CheckIPFilter(blocked, ctx.IP.String())
|
|
||||||
if err != nil {
|
|
||||||
rule.Log.App.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if match {
|
|
||||||
rule.Log.App.Debug().Str("ip", ctx.IP.String()).Str("item", blocked).Msg("IP is in block list, denying access")
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, allowed := range allowedIPs {
|
|
||||||
match, err := utils.CheckIPFilter(allowed, ctx.IP.String())
|
|
||||||
if err != nil {
|
|
||||||
rule.Log.App.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if match {
|
|
||||||
rule.Log.App.Debug().Str("ip", ctx.IP.String()).Str("item", allowed).Msg("IP is in allow list, allowing access")
|
|
||||||
return EffectAllow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(allowedIPs) > 0 {
|
|
||||||
rule.Log.App.Debug().Str("ip", ctx.IP.String()).Msg("IP not in allow list, denying access")
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
|
|
||||||
rule.Log.App.Debug().Str("ip", ctx.IP.String()).Msg("IP not in block or allow list, allowing access")
|
|
||||||
return EffectAllow
|
|
||||||
}
|
|
||||||
|
|
||||||
type IPBypassedRule struct {
|
|
||||||
Log *logger.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rule *IPBypassedRule) Evaluate(ctx *ACLContext) Effect {
|
|
||||||
if ctx.ACLs == nil {
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, bypassed := range ctx.ACLs.IP.Bypass {
|
|
||||||
match, err := utils.CheckIPFilter(bypassed, ctx.IP.String())
|
|
||||||
if err != nil {
|
|
||||||
rule.Log.App.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if match {
|
|
||||||
rule.Log.App.Debug().Str("ip", ctx.IP.String()).Str("item", bypassed).Msg("IP is in bypass list, skipping authentication")
|
|
||||||
return EffectAllow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rule.Log.App.Debug().Str("ip", ctx.IP.String()).Msg("IP not in bypass list, proceeding with authentication")
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
@@ -1,732 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUserAllowedRule(t *testing.T) {
|
|
||||||
log := logger.NewLogger().WithTestConfig()
|
|
||||||
log.Init()
|
|
||||||
|
|
||||||
rule := &UserAllowedRule{Log: log}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
ctx *ACLContext
|
|
||||||
expected Effect
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "abstains when ACLs are nil",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: nil,
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{Username: "alice"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectAbstain,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "abstains when user context is nil",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
OAuth: model.AppOAuth{Whitelist: "alice"},
|
|
||||||
},
|
|
||||||
UserContext: nil,
|
|
||||||
},
|
|
||||||
expected: EffectAbstain,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "allows OAuth user when email matches whitelist",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
OAuth: model.AppOAuth{Whitelist: "allowed@example.com"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderOAuth,
|
|
||||||
OAuth: &model.OAuthContext{
|
|
||||||
BaseContext: model.BaseContext{
|
|
||||||
Username: "different-username",
|
|
||||||
Email: "allowed@example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectAllow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies OAuth user when email does not match whitelist",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
OAuth: model.AppOAuth{Whitelist: "allowed@example.com"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderOAuth,
|
|
||||||
OAuth: &model.OAuthContext{
|
|
||||||
BaseContext: model.BaseContext{Email: "denied@example.com"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "abstains for OAuth user when whitelist filter is invalid",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
OAuth: model.AppOAuth{Whitelist: "/[/"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderOAuth,
|
|
||||||
OAuth: &model.OAuthContext{
|
|
||||||
BaseContext: model.BaseContext{Email: "allowed@example.com"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectAbstain,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies local user when username matches block list",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
Users: model.AppUsers{Block: "alice,bob"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{Username: "alice"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "allows local user when username does not match block list",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
Users: model.AppUsers{Block: "alice,bob"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{Username: "charlie"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectAllow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "abstains when block list filter is invalid",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
Users: model.AppUsers{Block: "/[/"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{Username: "alice"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectAbstain,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "allows local user when username matches allow list",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
Users: model.AppUsers{Allow: "alice,bob"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{Username: "alice"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectAllow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies local user when username does not match allow list",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
Users: model.AppUsers{Allow: "alice,bob"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{Username: "charlie"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "abstains when allow list filter is invalid",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
Users: model.AppUsers{Allow: "/[/"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{Username: "alice"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectAbstain,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
assert.Equal(t, tt.expected, rule.Evaluate(tt.ctx))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOAuthGroupRule(t *testing.T) {
|
|
||||||
log := logger.NewLogger().WithTestConfig()
|
|
||||||
log.Init()
|
|
||||||
|
|
||||||
rule := &OAuthGroupRule{Log: log}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
ctx *ACLContext
|
|
||||||
expected Effect
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "abstains when ACLs are nil",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: nil,
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderOAuth,
|
|
||||||
OAuth: &model.OAuthContext{
|
|
||||||
Groups: []string{"admins"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectAbstain,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "abstains when user context is nil",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
OAuth: model.AppOAuth{Whitelist: "alice"},
|
|
||||||
},
|
|
||||||
UserContext: nil,
|
|
||||||
},
|
|
||||||
expected: EffectAbstain,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "abstains when user is not OAuth",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
OAuth: model.AppOAuth{Groups: "admins"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{Username: "alice"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectAbstain,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "allows when provider is an override provider regardless of groups",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
OAuth: model.AppOAuth{Groups: "admins"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderOAuth,
|
|
||||||
OAuth: &model.OAuthContext{
|
|
||||||
ID: "google",
|
|
||||||
Groups: []string{"unrelated"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectAllow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "allows OAuth user when a group matches",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
OAuth: model.AppOAuth{Groups: "admins,users"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderOAuth,
|
|
||||||
OAuth: &model.OAuthContext{
|
|
||||||
ID: "custom",
|
|
||||||
Groups: []string{"users"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectAllow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies OAuth user when no group matches",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
OAuth: model.AppOAuth{Groups: "admins"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderOAuth,
|
|
||||||
OAuth: &model.OAuthContext{
|
|
||||||
ID: "custom",
|
|
||||||
Groups: []string{"users", "guests"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies OAuth user when user has no groups",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
OAuth: model.AppOAuth{Groups: "admins"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderOAuth,
|
|
||||||
OAuth: &model.OAuthContext{
|
|
||||||
ID: "custom",
|
|
||||||
Groups: nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "abstains when groups filter is invalid",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
OAuth: model.AppOAuth{Groups: "/[/"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderOAuth,
|
|
||||||
OAuth: &model.OAuthContext{
|
|
||||||
ID: "custom",
|
|
||||||
Groups: []string{"admins"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectAbstain,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
assert.Equal(t, tt.expected, rule.Evaluate(tt.ctx))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLDAPGroupRule(t *testing.T) {
|
|
||||||
log := logger.NewLogger().WithTestConfig()
|
|
||||||
log.Init()
|
|
||||||
|
|
||||||
rule := &LDAPGroupRule{Log: log}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
ctx *ACLContext
|
|
||||||
expected Effect
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "abstains when context is nil",
|
|
||||||
ctx: nil,
|
|
||||||
expected: EffectAbstain,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "abstains when user context is nil",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
OAuth: model.AppOAuth{Whitelist: "alice"},
|
|
||||||
},
|
|
||||||
UserContext: nil,
|
|
||||||
},
|
|
||||||
expected: EffectAbstain,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "abstains when user is not LDAP",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
LDAP: model.AppLDAP{Groups: "admins"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{Username: "alice"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectAbstain,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "allows LDAP user when a group matches",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
LDAP: model.AppLDAP{Groups: "admins,users"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderLDAP,
|
|
||||||
LDAP: &model.LDAPContext{
|
|
||||||
Groups: []string{"users"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectAllow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies LDAP user when no group matches",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
LDAP: model.AppLDAP{Groups: "admins"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderLDAP,
|
|
||||||
LDAP: &model.LDAPContext{
|
|
||||||
Groups: []string{"users", "guests"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies LDAP user when user has no groups",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
LDAP: model.AppLDAP{Groups: "admins"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderLDAP,
|
|
||||||
LDAP: &model.LDAPContext{
|
|
||||||
Groups: nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "abstains when groups filter is invalid",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
LDAP: model.AppLDAP{Groups: "/[/"},
|
|
||||||
},
|
|
||||||
UserContext: &model.UserContext{
|
|
||||||
Provider: model.ProviderLDAP,
|
|
||||||
LDAP: &model.LDAPContext{
|
|
||||||
Groups: []string{"admins"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: EffectAbstain,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
assert.Equal(t, tt.expected, rule.Evaluate(tt.ctx))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuthEnabledRule(t *testing.T) {
|
|
||||||
log := logger.NewLogger().WithTestConfig()
|
|
||||||
log.Init()
|
|
||||||
|
|
||||||
rule := &AuthEnabledRule{Log: log}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
ctx *ACLContext
|
|
||||||
expected Effect
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "deny when ACLs are nil",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: nil,
|
|
||||||
Path: "/anything",
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "allows when path does not match block regex",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
Path: model.AppPath{Block: "^/admin"},
|
|
||||||
},
|
|
||||||
Path: "/public",
|
|
||||||
},
|
|
||||||
expected: EffectAllow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies when path matches block regex and no allow regex",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
Path: model.AppPath{Block: "^/admin"},
|
|
||||||
},
|
|
||||||
Path: "/admin/users",
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "allows when path matches allow regex",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
Path: model.AppPath{Allow: "^/public"},
|
|
||||||
},
|
|
||||||
Path: "/public/index",
|
|
||||||
},
|
|
||||||
expected: EffectAllow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies when path does not match allow regex",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
Path: model.AppPath{Allow: "^/public"},
|
|
||||||
},
|
|
||||||
Path: "/private",
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "allows when blocked path is also explicitly allowed",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
Path: model.AppPath{
|
|
||||||
Block: "^/admin",
|
|
||||||
Allow: "^/admin/public",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Path: "/admin/public/page",
|
|
||||||
},
|
|
||||||
expected: EffectAllow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies when block regex fails to compile",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
Path: model.AppPath{Block: "[invalid"},
|
|
||||||
},
|
|
||||||
Path: "/anything",
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies when allow regex fails to compile",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
Path: model.AppPath{Allow: "[invalid"},
|
|
||||||
},
|
|
||||||
Path: "/anything",
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies when no path rules are configured",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{},
|
|
||||||
Path: "/anything",
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
assert.Equal(t, tt.expected, rule.Evaluate(tt.ctx))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIPAllowedRule(t *testing.T) {
|
|
||||||
log := logger.NewLogger().WithTestConfig()
|
|
||||||
log.Init()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
config model.Config
|
|
||||||
ctx *ACLContext
|
|
||||||
expected Effect
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "abstains when ACLs are nil",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: nil,
|
|
||||||
IP: net.ParseIP("10.0.0.1"),
|
|
||||||
},
|
|
||||||
expected: EffectAbstain,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies when IP matches app block list",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
IP: model.AppIP{Block: []string{"10.0.0.1"}},
|
|
||||||
},
|
|
||||||
IP: net.ParseIP("10.0.0.1"),
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies when IP matches global block list",
|
|
||||||
config: model.Config{
|
|
||||||
Auth: model.AuthConfig{
|
|
||||||
IP: model.IPConfig{Block: []string{"10.0.0.0/24"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{},
|
|
||||||
IP: net.ParseIP("10.0.0.5"),
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "allows when IP matches app allow list",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
IP: model.AppIP{Allow: []string{"192.168.1.0/24"}},
|
|
||||||
},
|
|
||||||
IP: net.ParseIP("192.168.1.10"),
|
|
||||||
},
|
|
||||||
expected: EffectAllow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "allows when IP matches global allow list",
|
|
||||||
config: model.Config{
|
|
||||||
Auth: model.AuthConfig{
|
|
||||||
IP: model.IPConfig{Allow: []string{"192.168.1.10"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{},
|
|
||||||
IP: net.ParseIP("192.168.1.10"),
|
|
||||||
},
|
|
||||||
expected: EffectAllow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies when allow list is set and IP does not match",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
IP: model.AppIP{Allow: []string{"192.168.1.0/24"}},
|
|
||||||
},
|
|
||||||
IP: net.ParseIP("10.0.0.1"),
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "allows when no block or allow lists are configured",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{},
|
|
||||||
IP: net.ParseIP("10.0.0.1"),
|
|
||||||
},
|
|
||||||
expected: EffectAllow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "block list takes precedence over allow list",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
IP: model.AppIP{
|
|
||||||
Block: []string{"10.0.0.1"},
|
|
||||||
Allow: []string{"10.0.0.1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
IP: net.ParseIP("10.0.0.1"),
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "skips invalid block entries and continues evaluation",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
IP: model.AppIP{
|
|
||||||
Block: []string{"not-an-ip"},
|
|
||||||
Allow: []string{"10.0.0.1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
IP: net.ParseIP("10.0.0.1"),
|
|
||||||
},
|
|
||||||
expected: EffectAllow,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
rule := &IPAllowedRule{Log: log, Config: tt.config}
|
|
||||||
assert.Equal(t, tt.expected, rule.Evaluate(tt.ctx))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIPBypassedRule(t *testing.T) {
|
|
||||||
log := logger.NewLogger().WithTestConfig()
|
|
||||||
log.Init()
|
|
||||||
|
|
||||||
rule := &IPBypassedRule{Log: log}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
ctx *ACLContext
|
|
||||||
expected Effect
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "deny when ACLs are nil",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: nil,
|
|
||||||
IP: net.ParseIP("10.0.0.1"),
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "allows when IP matches bypass list",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
|
|
||||||
},
|
|
||||||
IP: net.ParseIP("10.0.0.5"),
|
|
||||||
},
|
|
||||||
expected: EffectAllow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies when IP does not match bypass list",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
|
|
||||||
},
|
|
||||||
IP: net.ParseIP("192.168.1.1"),
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies when bypass list is empty",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{},
|
|
||||||
IP: net.ParseIP("10.0.0.1"),
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "skips invalid bypass entries and allows on later match",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
IP: model.AppIP{Bypass: []string{"not-an-ip", "10.0.0.1"}},
|
|
||||||
},
|
|
||||||
IP: net.ParseIP("10.0.0.1"),
|
|
||||||
},
|
|
||||||
expected: EffectAllow,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
assert.Equal(t, tt.expected, rule.Evaluate(tt.ctx))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,52 +13,51 @@ type LabelProvider interface {
|
|||||||
|
|
||||||
type AccessControlsService struct {
|
type AccessControlsService struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
config model.Config
|
|
||||||
labelProvider *LabelProvider
|
labelProvider *LabelProvider
|
||||||
|
static map[string]model.App
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAccessControlsService(
|
func NewAccessControlsService(
|
||||||
log *logger.Logger,
|
log *logger.Logger,
|
||||||
config model.Config,
|
labelProvider *LabelProvider,
|
||||||
labelProvider *LabelProvider) *AccessControlsService {
|
static map[string]model.App) *AccessControlsService {
|
||||||
|
|
||||||
return &AccessControlsService{
|
return &AccessControlsService{
|
||||||
log: log,
|
log: log,
|
||||||
config: config,
|
|
||||||
labelProvider: labelProvider,
|
labelProvider: labelProvider,
|
||||||
|
static: static,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *AccessControlsService) lookupStaticACLs(domain string) *model.App {
|
func (acls *AccessControlsService) lookupStaticACLs(domain string) *model.App {
|
||||||
var nameMatch *model.App
|
var appAcls *model.App
|
||||||
|
for app, config := range acls.static {
|
||||||
// First try to find a matching app by domain, then fallback to matching by app name (subdomain)
|
|
||||||
for app, config := range service.config.Apps {
|
|
||||||
if config.Config.Domain == domain {
|
if config.Config.Domain == domain {
|
||||||
service.log.App.Debug().Str("name", app).Msg("Found matching container by domain")
|
acls.log.App.Debug().Str("name", app).Msg("Found matching container by domain")
|
||||||
return &config
|
appAcls = &config
|
||||||
|
break // If we find a match by domain, we can stop searching
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.SplitN(domain, ".", 2)[0] == app {
|
if strings.SplitN(domain, ".", 2)[0] == app {
|
||||||
service.log.App.Debug().Str("name", app).Msg("Found matching container by app name")
|
acls.log.App.Debug().Str("name", app).Msg("Found matching container by app name")
|
||||||
nameMatch = &config
|
appAcls = &config
|
||||||
|
break // If we find a match by app name, we can stop searching
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return appAcls
|
||||||
return nameMatch
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *AccessControlsService) GetAccessControls(domain string) (*model.App, error) {
|
func (acls *AccessControlsService) GetAccessControls(domain string) (*model.App, error) {
|
||||||
// First check in the static config
|
// First check in the static config
|
||||||
app := service.lookupStaticACLs(domain)
|
app := acls.lookupStaticACLs(domain)
|
||||||
|
|
||||||
if app != nil {
|
if app != nil {
|
||||||
service.log.App.Debug().Msg("Using static ACLs for app")
|
acls.log.App.Debug().Msg("Using static ACLs for app")
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a label provider configured, try to get ACLs from it
|
// If we have a label provider configured, try to get ACLs from it
|
||||||
if service.labelProvider != nil && *service.labelProvider != nil {
|
if acls.labelProvider != nil {
|
||||||
return (*service.labelProvider).GetLabels(domain)
|
return (*acls.labelProvider).GetLabels(domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// no labels
|
// no labels
|
||||||
|
|||||||
@@ -1,199 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type mockLabelProvider struct {
|
|
||||||
getLabelsFn func(appDomain string) (*model.App, error)
|
|
||||||
calledWith string
|
|
||||||
callCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockLabelProvider) GetLabels(appDomain string) (*model.App, error) {
|
|
||||||
m.calledWith = appDomain
|
|
||||||
m.callCount++
|
|
||||||
if m.getLabelsFn != nil {
|
|
||||||
return m.getLabelsFn(appDomain)
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLookupStaticACLs(t *testing.T) {
|
|
||||||
log := logger.NewLogger().WithTestConfig()
|
|
||||||
log.Init()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
apps map[string]model.App
|
|
||||||
domain string
|
|
||||||
expectNil bool
|
|
||||||
expectedDomain string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "returns nil when no apps are configured",
|
|
||||||
apps: nil,
|
|
||||||
domain: "foo.example.com",
|
|
||||||
expectNil: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "returns nil when no app matches",
|
|
||||||
apps: map[string]model.App{
|
|
||||||
"foo": {Config: model.AppConfig{Domain: "foo.example.com"}},
|
|
||||||
},
|
|
||||||
domain: "bar.example.com",
|
|
||||||
expectNil: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "matches by exact domain",
|
|
||||||
apps: map[string]model.App{
|
|
||||||
"foo": {Config: model.AppConfig{Domain: "foo.example.com"}},
|
|
||||||
},
|
|
||||||
domain: "foo.example.com",
|
|
||||||
expectedDomain: "foo.example.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "matches by app name when domain does not match any app",
|
|
||||||
apps: map[string]model.App{
|
|
||||||
"foo": {Config: model.AppConfig{Domain: "configured.example.com"}},
|
|
||||||
},
|
|
||||||
domain: "foo.example.com",
|
|
||||||
expectedDomain: "configured.example.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "matches by app name for nested subdomains",
|
|
||||||
apps: map[string]model.App{
|
|
||||||
"foo": {Config: model.AppConfig{Domain: "configured.example.com"}},
|
|
||||||
},
|
|
||||||
domain: "foo.sub.example.com",
|
|
||||||
expectedDomain: "configured.example.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "selects the app matching by domain among multiple apps",
|
|
||||||
apps: map[string]model.App{
|
|
||||||
"unrelated": {Config: model.AppConfig{Domain: "other.example.com"}},
|
|
||||||
"target": {Config: model.AppConfig{Domain: "foo.example.com"}},
|
|
||||||
},
|
|
||||||
domain: "foo.example.com",
|
|
||||||
expectedDomain: "foo.example.com",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
svc := NewAccessControlsService(log, model.Config{Apps: tt.apps}, nil)
|
|
||||||
got := svc.lookupStaticACLs(tt.domain)
|
|
||||||
if tt.expectNil {
|
|
||||||
assert.Nil(t, got)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NotNil(t, got)
|
|
||||||
assert.Equal(t, tt.expectedDomain, got.Config.Domain)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetAccessControls(t *testing.T) {
|
|
||||||
log := logger.NewLogger().WithTestConfig()
|
|
||||||
log.Init()
|
|
||||||
|
|
||||||
t.Run("returns static ACLs when domain matches", func(t *testing.T) {
|
|
||||||
config := model.Config{
|
|
||||||
Apps: map[string]model.App{
|
|
||||||
"foo": {
|
|
||||||
Config: model.AppConfig{Domain: "foo.example.com"},
|
|
||||||
Users: model.AppUsers{Allow: "alice"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
svc := NewAccessControlsService(log, config, nil)
|
|
||||||
|
|
||||||
got, err := svc.GetAccessControls("foo.example.com")
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, got)
|
|
||||||
assert.Equal(t, "foo.example.com", got.Config.Domain)
|
|
||||||
assert.Equal(t, "alice", got.Users.Allow)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("returns nil when no static match and no label provider", func(t *testing.T) {
|
|
||||||
svc := NewAccessControlsService(log, model.Config{}, nil)
|
|
||||||
|
|
||||||
got, err := svc.GetAccessControls("unknown.example.com")
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Nil(t, got)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("returns nil when label provider pointer wraps a nil interface", func(t *testing.T) {
|
|
||||||
var provider LabelProvider
|
|
||||||
svc := NewAccessControlsService(log, model.Config{}, &provider)
|
|
||||||
|
|
||||||
got, err := svc.GetAccessControls("unknown.example.com")
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Nil(t, got)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("falls back to label provider when no static match", func(t *testing.T) {
|
|
||||||
expected := &model.App{
|
|
||||||
Config: model.AppConfig{Domain: "dynamic.example.com"},
|
|
||||||
Users: model.AppUsers{Allow: "bob"},
|
|
||||||
}
|
|
||||||
mock := &mockLabelProvider{
|
|
||||||
getLabelsFn: func(appDomain string) (*model.App, error) {
|
|
||||||
return expected, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
var provider LabelProvider = mock
|
|
||||||
svc := NewAccessControlsService(log, model.Config{}, &provider)
|
|
||||||
|
|
||||||
got, err := svc.GetAccessControls("dynamic.example.com")
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Same(t, expected, got)
|
|
||||||
assert.Equal(t, "dynamic.example.com", mock.calledWith)
|
|
||||||
assert.Equal(t, 1, mock.callCount)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("does not call label provider when static match found", func(t *testing.T) {
|
|
||||||
mock := &mockLabelProvider{}
|
|
||||||
var provider LabelProvider = mock
|
|
||||||
config := model.Config{
|
|
||||||
Apps: map[string]model.App{
|
|
||||||
"foo": {Config: model.AppConfig{Domain: "foo.example.com"}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
svc := NewAccessControlsService(log, config, &provider)
|
|
||||||
|
|
||||||
got, err := svc.GetAccessControls("foo.example.com")
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, got)
|
|
||||||
assert.Equal(t, "foo.example.com", got.Config.Domain)
|
|
||||||
assert.Equal(t, 0, mock.callCount)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("propagates label provider errors", func(t *testing.T) {
|
|
||||||
providerErr := errors.New("provider boom")
|
|
||||||
mock := &mockLabelProvider{
|
|
||||||
getLabelsFn: func(appDomain string) (*model.App, error) {
|
|
||||||
return nil, providerErr
|
|
||||||
},
|
|
||||||
}
|
|
||||||
var provider LabelProvider = mock
|
|
||||||
svc := NewAccessControlsService(log, model.Config{}, &provider)
|
|
||||||
|
|
||||||
got, err := svc.GetAccessControls("dynamic.example.com")
|
|
||||||
|
|
||||||
assert.Nil(t, got)
|
|
||||||
assert.ErrorIs(t, err, providerErr)
|
|
||||||
assert.Equal(t, 1, mock.callCount)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,11 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -16,6 +18,7 @@ import (
|
|||||||
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
@@ -76,9 +79,8 @@ type AuthService struct {
|
|||||||
context context.Context
|
context context.Context
|
||||||
|
|
||||||
ldap *LdapService
|
ldap *LdapService
|
||||||
queries repository.Store
|
queries *repository.Queries
|
||||||
oauthBroker *OAuthBrokerService
|
oauthBroker *OAuthBrokerService
|
||||||
tailscale *TailscaleService
|
|
||||||
|
|
||||||
loginAttempts map[string]*LoginAttempt
|
loginAttempts map[string]*LoginAttempt
|
||||||
ldapGroupsCache map[string]*LdapGroupsCache
|
ldapGroupsCache map[string]*LdapGroupsCache
|
||||||
@@ -98,9 +100,8 @@ func NewAuthService(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
wg *sync.WaitGroup,
|
wg *sync.WaitGroup,
|
||||||
ldap *LdapService,
|
ldap *LdapService,
|
||||||
queries repository.Store,
|
queries *repository.Queries,
|
||||||
oauthBroker *OAuthBrokerService,
|
oauthBroker *OAuthBrokerService,
|
||||||
tailscale *TailscaleService,
|
|
||||||
) *AuthService {
|
) *AuthService {
|
||||||
service := &AuthService{
|
service := &AuthService{
|
||||||
log: log,
|
log: log,
|
||||||
@@ -113,7 +114,6 @@ func NewAuthService(
|
|||||||
ldap: ldap,
|
ldap: ldap,
|
||||||
queries: queries,
|
queries: queries,
|
||||||
oauthBroker: oauthBroker,
|
oauthBroker: oauthBroker,
|
||||||
tailscale: tailscale,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Go(service.CleanupOAuthSessionsRoutine)
|
wg.Go(service.CleanupOAuthSessionsRoutine)
|
||||||
@@ -286,19 +286,10 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsEmailWhitelisted(email string) bool {
|
func (auth *AuthService) IsEmailWhitelisted(email string) bool {
|
||||||
match, err := utils.CheckFilter(strings.Join(auth.runtime.OAuthWhitelist, ","), email)
|
return utils.CheckFilter(strings.Join(auth.runtime.OAuthWhitelist, ","), email)
|
||||||
if err != nil {
|
|
||||||
auth.log.App.Warn().Err(err).Str("email", email).Msg("Invalid email filter pattern")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return match
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) CreateSession(ctx context.Context, data repository.Session) (*http.Cookie, error) {
|
func (auth *AuthService) CreateSession(ctx context.Context, data repository.Session) (*http.Cookie, error) {
|
||||||
if data.Provider == "tailscale" && auth.tailscale == nil {
|
|
||||||
return nil, fmt.Errorf("tailscale service not configured, cannot create session for tailscale user")
|
|
||||||
}
|
|
||||||
|
|
||||||
uuid, err := uuid.NewRandom()
|
uuid, err := uuid.NewRandom()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -335,28 +326,6 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess
|
|||||||
return nil, fmt.Errorf("failed to create session entry: %w", err)
|
return nil, fmt.Errorf("failed to create session entry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.Provider == "tailscale" {
|
|
||||||
auth.log.App.Trace().Str("url", fmt.Sprintf("https://%s", auth.tailscale.GetHostname())).Msg("Extracting root domain from Tailscale hostname")
|
|
||||||
|
|
||||||
tsCookieDomain, err := utils.GetCookieDomain(fmt.Sprintf("https://%s", auth.tailscale.GetHostname()))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get cookie domain for tailscale user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &http.Cookie{
|
|
||||||
Name: auth.runtime.SessionCookieName,
|
|
||||||
Value: session.UUID,
|
|
||||||
Path: "/",
|
|
||||||
Domain: fmt.Sprintf(".%s", tsCookieDomain),
|
|
||||||
Expires: expiresAt,
|
|
||||||
MaxAge: int(time.Until(expiresAt).Seconds()),
|
|
||||||
Secure: auth.config.Auth.SecureCookie,
|
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &http.Cookie{
|
return &http.Cookie{
|
||||||
Name: auth.runtime.SessionCookieName,
|
Name: auth.runtime.SessionCookieName,
|
||||||
Value: session.UUID,
|
Value: session.UUID,
|
||||||
@@ -448,7 +417,7 @@ func (auth *AuthService) GetSession(ctx context.Context, uuid string) (*reposito
|
|||||||
session, err := auth.queries.GetSession(ctx, uuid)
|
session, err := auth.queries.GetSession(ctx, uuid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrNotFound) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, errors.New("session not found")
|
return nil, errors.New("session not found")
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -485,6 +454,171 @@ func (auth *AuthService) LDAPAuthConfigured() bool {
|
|||||||
return auth.ldap != nil
|
return auth.ldap != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) IsUserAllowed(c *gin.Context, context model.UserContext, acls *model.App) bool {
|
||||||
|
if acls == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if context.Provider == model.ProviderOAuth {
|
||||||
|
auth.log.App.Debug().Msg("User is an OAuth user, checking OAuth whitelist")
|
||||||
|
return utils.CheckFilter(acls.OAuth.Whitelist, context.OAuth.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
if acls.Users.Block != "" {
|
||||||
|
auth.log.App.Debug().Msg("Checking users block list")
|
||||||
|
if utils.CheckFilter(acls.Users.Block, context.GetUsername()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.log.App.Debug().Msg("Checking users allow list")
|
||||||
|
return utils.CheckFilter(acls.Users.Allow, context.GetUsername())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context model.UserContext, acls *model.App) bool {
|
||||||
|
if acls == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !context.IsOAuth() {
|
||||||
|
auth.log.App.Debug().Msg("User is not an OAuth user, skipping OAuth group check")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := model.OverrideProviders[context.OAuth.ID]; ok {
|
||||||
|
auth.log.App.Debug().Str("provider", context.OAuth.ID).Msg("Provider override detected, skipping group check")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, userGroup := range context.OAuth.Groups {
|
||||||
|
if utils.CheckFilter(acls.OAuth.Groups, strings.TrimSpace(userGroup)) {
|
||||||
|
auth.log.App.Trace().Str("group", userGroup).Str("required", acls.OAuth.Groups).Msg("User group matched")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.log.App.Debug().Msg("No groups matched")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) IsInLDAPGroup(c *gin.Context, context model.UserContext, acls *model.App) bool {
|
||||||
|
if acls == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !context.IsLDAP() {
|
||||||
|
auth.log.App.Debug().Msg("User is not an LDAP user, skipping LDAP group check")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, userGroup := range context.LDAP.Groups {
|
||||||
|
if utils.CheckFilter(acls.LDAP.Groups, strings.TrimSpace(userGroup)) {
|
||||||
|
auth.log.App.Trace().Str("group", userGroup).Str("required", acls.LDAP.Groups).Msg("User group matched")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.log.App.Debug().Msg("No groups matched")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) IsAuthEnabled(uri string, acls *model.App) (bool, error) {
|
||||||
|
if acls == nil {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for block list
|
||||||
|
if acls.Path.Block != "" {
|
||||||
|
regex, err := regexp.Compile(acls.Path.Block)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !regex.MatchString(uri) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for allow list
|
||||||
|
if acls.Path.Allow != "" {
|
||||||
|
regex, err := regexp.Compile(acls.Path.Allow)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if regex.MatchString(uri) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) CheckIP(ip string, acls *model.App) bool {
|
||||||
|
if acls == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the global and app IP filter
|
||||||
|
blockedIps := append(auth.config.Auth.IP.Block, acls.IP.Block...)
|
||||||
|
allowedIPs := append(auth.config.Auth.IP.Allow, acls.IP.Allow...)
|
||||||
|
|
||||||
|
for _, blocked := range blockedIps {
|
||||||
|
res, err := utils.FilterIP(blocked, ip)
|
||||||
|
if err != nil {
|
||||||
|
auth.log.App.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if res {
|
||||||
|
auth.log.App.Debug().Str("ip", ip).Str("item", blocked).Msg("IP is in block list, denying access")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, allowed := range allowedIPs {
|
||||||
|
res, err := utils.FilterIP(allowed, ip)
|
||||||
|
if err != nil {
|
||||||
|
auth.log.App.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if res {
|
||||||
|
auth.log.App.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allow list, allowing access")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allowedIPs) > 0 {
|
||||||
|
auth.log.App.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.log.App.Debug().Str("ip", ip).Msg("IP not in any block or allow list, allowing access by default")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) IsBypassedIP(ip string, acls *model.App) bool {
|
||||||
|
if acls == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bypassed := range acls.IP.Bypass {
|
||||||
|
res, err := utils.FilterIP(bypassed, ip)
|
||||||
|
if err != nil {
|
||||||
|
auth.log.App.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if res {
|
||||||
|
auth.log.App.Debug().Str("ip", ip).Str("item", bypassed).Msg("IP is in bypass list, skipping authentication")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.log.App.Debug().Str("ip", ip).Msg("IP not in bypass list, proceeding with authentication")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (auth *AuthService) NewOAuthSession(serviceName string, params OAuthURLParams) (string, OAuthPendingSession, error) {
|
func (auth *AuthService) NewOAuthSession(serviceName string, params OAuthURLParams) (string, OAuthPendingSession, error) {
|
||||||
auth.ensureOAuthSessionLimit()
|
auth.ensureOAuthSessionLimit()
|
||||||
|
|
||||||
|
|||||||
@@ -85,23 +85,17 @@ func (docker *DockerService) GetLabels(appDomain string) (*model.App, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var nameMatch *model.App
|
|
||||||
|
|
||||||
// First try to find a matching app by domain, then fallback to matching by app name (subdomain)
|
|
||||||
for appName, appLabels := range labels.Apps {
|
for appName, appLabels := range labels.Apps {
|
||||||
if appLabels.Config.Domain == appDomain {
|
if appLabels.Config.Domain == appDomain {
|
||||||
docker.log.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by domain")
|
docker.log.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by domain")
|
||||||
return &appLabels, nil
|
return &appLabels, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.SplitN(appDomain, ".", 2)[0] == appName {
|
if strings.SplitN(appDomain, ".", 2)[0] == appName {
|
||||||
docker.log.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name")
|
docker.log.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name")
|
||||||
nameMatch = &appLabels
|
return &appLabels, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if nameMatch != nil {
|
|
||||||
return nameMatch, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
docker.log.App.Debug().Str("domain", appDomain).Msg("No matching container found for domain")
|
docker.log.App.Debug().Str("domain", appDomain).Msg("No matching container found for domain")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"database/sql"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
@@ -115,7 +116,7 @@ type OIDCService struct {
|
|||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
config model.Config
|
config model.Config
|
||||||
runtime model.RuntimeConfig
|
runtime model.RuntimeConfig
|
||||||
queries repository.Store
|
queries *repository.Queries
|
||||||
context context.Context
|
context context.Context
|
||||||
|
|
||||||
clients map[string]model.OIDCClientConfig
|
clients map[string]model.OIDCClientConfig
|
||||||
@@ -128,7 +129,7 @@ func NewOIDCService(
|
|||||||
log *logger.Logger,
|
log *logger.Logger,
|
||||||
config model.Config,
|
config model.Config,
|
||||||
runtime model.RuntimeConfig,
|
runtime model.RuntimeConfig,
|
||||||
queries repository.Store,
|
queries *repository.Queries,
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
wg *sync.WaitGroup) (*OIDCService, error) {
|
wg *sync.WaitGroup) (*OIDCService, error) {
|
||||||
// If not configured, skip init
|
// If not configured, skip init
|
||||||
@@ -433,7 +434,7 @@ func (service *OIDCService) GetCodeEntry(c *gin.Context, codeHash string, client
|
|||||||
oidcCode, err := service.queries.GetOidcCode(c, codeHash)
|
oidcCode, err := service.queries.GetOidcCode(c, codeHash)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrNotFound) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return repository.OidcCode{}, ErrCodeNotFound
|
return repository.OidcCode{}, ErrCodeNotFound
|
||||||
}
|
}
|
||||||
return repository.OidcCode{}, err
|
return repository.OidcCode{}, err
|
||||||
@@ -577,7 +578,7 @@ func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken stri
|
|||||||
entry, err := service.queries.GetOidcTokenByRefreshToken(c, service.Hash(refreshToken))
|
entry, err := service.queries.GetOidcTokenByRefreshToken(c, service.Hash(refreshToken))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrNotFound) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return TokenResponse{}, ErrTokenNotFound
|
return TokenResponse{}, ErrTokenNotFound
|
||||||
}
|
}
|
||||||
return TokenResponse{}, err
|
return TokenResponse{}, err
|
||||||
@@ -656,7 +657,7 @@ func (service *OIDCService) GetAccessToken(c *gin.Context, tokenHash string) (re
|
|||||||
entry, err := service.queries.GetOidcToken(c, tokenHash)
|
entry, err := service.queries.GetOidcToken(c, tokenHash)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrNotFound) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return repository.OidcToken{}, ErrTokenNotFound
|
return repository.OidcToken{}, ErrTokenNotFound
|
||||||
}
|
}
|
||||||
return repository.OidcToken{}, err
|
return repository.OidcToken{}, err
|
||||||
@@ -744,15 +745,15 @@ func (service *OIDCService) Hash(token string) string {
|
|||||||
|
|
||||||
func (service *OIDCService) DeleteOldSession(ctx context.Context, sub string) error {
|
func (service *OIDCService) DeleteOldSession(ctx context.Context, sub string) error {
|
||||||
err := service.queries.DeleteOidcCodeBySub(ctx, sub)
|
err := service.queries.DeleteOidcCodeBySub(ctx, sub)
|
||||||
if err != nil && !errors.Is(err, repository.ErrNotFound) {
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = service.queries.DeleteOidcTokenBySub(ctx, sub)
|
err = service.queries.DeleteOidcTokenBySub(ctx, sub)
|
||||||
if err != nil && !errors.Is(err, repository.ErrNotFound) {
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = service.queries.DeleteOidcUserInfo(ctx, sub)
|
err = service.queries.DeleteOidcUserInfo(ctx, sub)
|
||||||
if err != nil && !errors.Is(err, repository.ErrNotFound) {
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -792,16 +793,14 @@ func (service *OIDCService) cleanupRoutine() {
|
|||||||
expiredCodes, err := service.queries.DeleteExpiredOidcCodes(service.context, currentTime)
|
expiredCodes, err := service.queries.DeleteExpiredOidcCodes(service.context, currentTime)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
service.log.App.Warn().Err(err).Msg("Failed to delete expired codes")
|
service.log.App.Warn().Err(err).Msg("Failed to delete expired codes")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, expiredCode := range expiredCodes {
|
for _, expiredCode := range expiredCodes {
|
||||||
token, err := service.queries.GetOidcTokenBySub(service.context, expiredCode.Sub)
|
token, err := service.queries.GetOidcTokenBySub(service.context, expiredCode.Sub)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, repository.ErrNotFound) {
|
service.log.App.Warn().Err(err).Msg("Failed to get token by sub for expired code")
|
||||||
service.log.App.Warn().Err(err).Msg("Failed to get token by sub for expired code")
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Policy string
|
|
||||||
|
|
||||||
const (
|
|
||||||
PolicyAllow Policy = "allow"
|
|
||||||
PolicyDeny Policy = "deny"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Effect int
|
|
||||||
|
|
||||||
const (
|
|
||||||
EffectAbstain Effect = iota
|
|
||||||
EffectAllow
|
|
||||||
EffectDeny
|
|
||||||
)
|
|
||||||
|
|
||||||
type Rule interface {
|
|
||||||
Evaluate(ctx *ACLContext) Effect
|
|
||||||
}
|
|
||||||
|
|
||||||
type ACLContext struct {
|
|
||||||
ACLs *model.App
|
|
||||||
UserContext *model.UserContext
|
|
||||||
IP net.IP
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyEngine struct {
|
|
||||||
log *logger.Logger
|
|
||||||
rules map[RuleName]Rule
|
|
||||||
policy Policy
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPolicyEngine(config model.Config, log *logger.Logger) (*PolicyEngine, error) {
|
|
||||||
engine := PolicyEngine{
|
|
||||||
log: log,
|
|
||||||
rules: make(map[RuleName]Rule),
|
|
||||||
}
|
|
||||||
|
|
||||||
switch config.Auth.ACLs.Policy {
|
|
||||||
case string(PolicyAllow):
|
|
||||||
log.App.Debug().Msg("Using 'allow' ACL policy: access to apps will be allowed by default unless explicitly blocked")
|
|
||||||
engine.policy = PolicyAllow
|
|
||||||
case string(PolicyDeny):
|
|
||||||
log.App.Debug().Msg("Using 'deny' ACL policy: access to apps will be blocked by default unless explicitly allowed")
|
|
||||||
engine.policy = PolicyDeny
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid acl policy: %s", config.Auth.ACLs.Policy)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &engine, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (engine *PolicyEngine) RegisterRule(name RuleName, rule Rule) {
|
|
||||||
engine.log.App.Debug().Str("rule", string(name)).Msg("Registering ACL rule in policy engine")
|
|
||||||
engine.rules[name] = rule
|
|
||||||
}
|
|
||||||
|
|
||||||
func (engine *PolicyEngine) evaluateRuleByName(name RuleName, ctx *ACLContext) Effect {
|
|
||||||
rule, exists := engine.rules[name]
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
engine.log.App.Warn().Str("rule", string(name)).Msg("Rule not found in policy engine, defaulting to deny")
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
|
|
||||||
return rule.Evaluate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (engine *PolicyEngine) effectToAccess(effect Effect) bool {
|
|
||||||
switch effect {
|
|
||||||
case EffectAllow:
|
|
||||||
return true
|
|
||||||
case EffectDeny:
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
// If the effect is abstain, we fall back to the default policy
|
|
||||||
return engine.policy == PolicyAllow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (engine *PolicyEngine) Evaluate(name RuleName, ctx *ACLContext) bool {
|
|
||||||
effect := engine.evaluateRuleByName(name, ctx)
|
|
||||||
access := engine.effectToAccess(effect)
|
|
||||||
|
|
||||||
engine.log.App.Debug().
|
|
||||||
Str("rule", string(name)).
|
|
||||||
Int("effect", int(effect)).
|
|
||||||
Bool("access", access).
|
|
||||||
Msg("Evaluated ACL rule")
|
|
||||||
|
|
||||||
return access
|
|
||||||
}
|
|
||||||
|
|
||||||
func (engine *PolicyEngine) Policy() Policy {
|
|
||||||
return engine.policy
|
|
||||||
}
|
|
||||||
|
|
||||||
func (engine *PolicyEngine) Rules() map[RuleName]Rule {
|
|
||||||
return engine.rules
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package service_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/test"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create test rule
|
|
||||||
type TestRule struct{}
|
|
||||||
|
|
||||||
func (rule *TestRule) Evaluate(ctx *service.ACLContext) service.Effect {
|
|
||||||
switch ctx.Path {
|
|
||||||
case "/allowed":
|
|
||||||
return service.EffectAllow
|
|
||||||
case "/denied":
|
|
||||||
return service.EffectDeny
|
|
||||||
default:
|
|
||||||
return service.EffectAbstain
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPolicyEngine(t *testing.T) {
|
|
||||||
log := logger.NewLogger().WithTestConfig()
|
|
||||||
log.Init()
|
|
||||||
|
|
||||||
cfg, _ := test.CreateTestConfigs(t)
|
|
||||||
|
|
||||||
testRule := &TestRule{}
|
|
||||||
|
|
||||||
// Engine should fail with invalid policy
|
|
||||||
cfg.Auth.ACLs.Policy = "invalid_policy"
|
|
||||||
_, err := service.NewPolicyEngine(cfg, log)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
// Engine should initialize with 'allow' policy
|
|
||||||
cfg.Auth.ACLs.Policy = string(service.PolicyAllow)
|
|
||||||
engine, err := service.NewPolicyEngine(cfg, log)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, service.PolicyAllow, engine.Policy())
|
|
||||||
|
|
||||||
// Engine should initialize with 'deny' policy
|
|
||||||
cfg.Auth.ACLs.Policy = string(service.PolicyDeny)
|
|
||||||
engine, err = service.NewPolicyEngine(cfg, log)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, service.PolicyDeny, engine.Policy())
|
|
||||||
|
|
||||||
// Engine should allow adding rules
|
|
||||||
engine, err = service.NewPolicyEngine(cfg, log)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
engine.RegisterRule("test-rule", testRule)
|
|
||||||
_, ok := engine.Rules()["test-rule"]
|
|
||||||
assert.True(t, ok)
|
|
||||||
|
|
||||||
// Begin allow policy tests
|
|
||||||
cfg.Auth.ACLs.Policy = string(service.PolicyAllow)
|
|
||||||
engine, err = service.NewPolicyEngine(cfg, log)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
engine.RegisterRule("test-rule", testRule)
|
|
||||||
|
|
||||||
// With allow policy, if rule allows, access should be allowed
|
|
||||||
ctx := &service.ACLContext{Path: "/allowed"}
|
|
||||||
assert.Equal(t, true, engine.Evaluate("test-rule", ctx))
|
|
||||||
|
|
||||||
// With allow policy, if rule denies, access should be denied
|
|
||||||
ctx.Path = "/denied"
|
|
||||||
assert.Equal(t, false, engine.Evaluate("test-rule", ctx))
|
|
||||||
|
|
||||||
// With allow policy, if rule abstains, access should be allowed (default)
|
|
||||||
ctx.Path = "/abstain"
|
|
||||||
assert.Equal(t, true, engine.Evaluate("test-rule", ctx))
|
|
||||||
|
|
||||||
// Begin deny policy tests
|
|
||||||
cfg.Auth.ACLs.Policy = string(service.PolicyDeny)
|
|
||||||
engine, err = service.NewPolicyEngine(cfg, log)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
engine.RegisterRule("test-rule", testRule)
|
|
||||||
|
|
||||||
// With deny policy, if rule allows, access should be allowed
|
|
||||||
ctx.Path = "/allowed"
|
|
||||||
assert.Equal(t, true, engine.Evaluate("test-rule", ctx))
|
|
||||||
|
|
||||||
// With deny policy, if rule denies, access should be denied
|
|
||||||
ctx.Path = "/denied"
|
|
||||||
assert.Equal(t, false, engine.Evaluate("test-rule", ctx))
|
|
||||||
|
|
||||||
// With deny policy, if rule abstains, access should be denied (default)
|
|
||||||
ctx.Path = "/abstain"
|
|
||||||
assert.Equal(t, false, engine.Evaluate("test-rule", ctx))
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
|
||||||
"tailscale.com/client/local"
|
|
||||||
"tailscale.com/tsnet"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TailscaleWhoisResponse struct {
|
|
||||||
UserID string
|
|
||||||
LoginName string
|
|
||||||
DisplayName string
|
|
||||||
NodeName string
|
|
||||||
Tags []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type TailscaleService struct {
|
|
||||||
log *logger.Logger
|
|
||||||
wg *sync.WaitGroup
|
|
||||||
config model.Config
|
|
||||||
ctx context.Context
|
|
||||||
|
|
||||||
srv *tsnet.Server
|
|
||||||
lc *local.Client
|
|
||||||
ln *net.Listener
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Context, wg *sync.WaitGroup) (*TailscaleService, error) {
|
|
||||||
if !config.Tailscale.Enabled {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
srv := new(tsnet.Server)
|
|
||||||
|
|
||||||
// node options
|
|
||||||
srv.Dir = config.Tailscale.Dir
|
|
||||||
srv.Hostname = config.Tailscale.Hostname
|
|
||||||
srv.AuthKey = config.Tailscale.AuthKey
|
|
||||||
srv.Ephemeral = config.Tailscale.Ephemeral
|
|
||||||
|
|
||||||
// redirect logs to zerolog
|
|
||||||
srv.Logf = log.App.Printf
|
|
||||||
srv.UserLogf = log.App.Printf
|
|
||||||
|
|
||||||
err := srv.Start()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to start tailscale server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
lc, err := srv.LocalClient()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
_ = srv.Close()
|
|
||||||
return nil, fmt.Errorf("failed to get tailscale local client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
service := &TailscaleService{
|
|
||||||
log: log,
|
|
||||||
wg: wg,
|
|
||||||
config: config,
|
|
||||||
ctx: ctx,
|
|
||||||
srv: srv,
|
|
||||||
lc: lc,
|
|
||||||
}
|
|
||||||
|
|
||||||
connectCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) // large enough timeout to allow for user to manually authenticate with link if needed
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err = service.waitForConn(connectCtx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
_ = srv.Close()
|
|
||||||
return nil, fmt.Errorf("failed to connect to tailscale network: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Go(service.watchAndClose)
|
|
||||||
|
|
||||||
return service, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TailscaleService) watchAndClose() {
|
|
||||||
<-ts.ctx.Done()
|
|
||||||
ts.log.App.Debug().Msg("Shutting down Tailscale service")
|
|
||||||
ts.mu.Lock()
|
|
||||||
srv := ts.srv
|
|
||||||
ln := ts.ln
|
|
||||||
ts.ln = nil
|
|
||||||
ts.srv = nil
|
|
||||||
ts.mu.Unlock()
|
|
||||||
if ln != nil {
|
|
||||||
(*ts.ln).Close()
|
|
||||||
}
|
|
||||||
if srv != nil {
|
|
||||||
ts.srv.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TailscaleService) Whois(ctx context.Context, addr string) (*TailscaleWhoisResponse, error) {
|
|
||||||
who, err := ts.lc.WhoIs(ctx, addr)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, local.ErrPeerNotFound) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("failed to get client whois: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
res := TailscaleWhoisResponse{
|
|
||||||
UserID: who.UserProfile.ID.String(),
|
|
||||||
LoginName: who.UserProfile.LoginName,
|
|
||||||
DisplayName: who.UserProfile.DisplayName,
|
|
||||||
NodeName: strings.TrimSuffix(who.Node.Name, "."),
|
|
||||||
Tags: who.Node.Tags,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TailscaleService) CreateListener() (net.Listener, error) {
|
|
||||||
ts.mu.Lock()
|
|
||||||
defer ts.mu.Unlock()
|
|
||||||
|
|
||||||
if ts.ln != nil {
|
|
||||||
return *ts.ln, nil
|
|
||||||
}
|
|
||||||
ln, err := ts.srv.ListenTLS("tcp", ":443")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ts.ln = &ln
|
|
||||||
return ln, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TailscaleService) GetHostname() string {
|
|
||||||
status, err := ts.lc.Status(ts.ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ts.log.App.Error().Err(err).Msg("Failed to get Tailscale status")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.TrimSuffix(status.Self.DNSName, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TailscaleService) waitForConn(ctx context.Context) error {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return fmt.Errorf("timed out waiting for tailscale connection")
|
|
||||||
default:
|
|
||||||
ip4, _ := ts.srv.TailscaleIPs()
|
|
||||||
if !ip4.IsValid() {
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -40,9 +40,6 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
|
|||||||
SessionExpiry: 10,
|
SessionExpiry: 10,
|
||||||
LoginTimeout: 10,
|
LoginTimeout: 10,
|
||||||
LoginMaxRetries: 3,
|
LoginMaxRetries: 3,
|
||||||
ACLs: model.ACLsConfig{
|
|
||||||
Policy: "allow",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Database: model.DatabaseConfig{
|
Database: model.DatabaseConfig{
|
||||||
Path: filepath.Join(tempDir, "test.db"),
|
Path: filepath.Join(tempDir, "test.db"),
|
||||||
@@ -51,32 +48,6 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
Path: filepath.Join(tempDir, "resources"),
|
Path: filepath.Join(tempDir, "resources"),
|
||||||
},
|
},
|
||||||
Apps: map[string]model.App{
|
|
||||||
"app_path_allow": {
|
|
||||||
Config: model.AppConfig{
|
|
||||||
Domain: "path-allow.example.com",
|
|
||||||
},
|
|
||||||
Path: model.AppPath{
|
|
||||||
Allow: "/allowed",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"app_user_allow": {
|
|
||||||
Config: model.AppConfig{
|
|
||||||
Domain: "user-allow.example.com",
|
|
||||||
},
|
|
||||||
Users: model.AppUsers{
|
|
||||||
Allow: "testuser",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"ip_bypass": {
|
|
||||||
Config: model.AppConfig{
|
|
||||||
Domain: "ip-bypass.example.com",
|
|
||||||
},
|
|
||||||
IP: model.AppIP{
|
|
||||||
Bypass: []string{"10.10.10.10"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
passwd, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)
|
passwd, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -46,27 +46,26 @@ func EncodeBasicAuth(username string, password string) string {
|
|||||||
return base64.StdEncoding.EncodeToString([]byte(auth))
|
return base64.StdEncoding.EncodeToString([]byte(auth))
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckIPFilter(filter string, ip string) (bool, error) {
|
func FilterIP(filter string, ip string) (bool, error) {
|
||||||
ipAddr := net.ParseIP(ip)
|
ipAddr := net.ParseIP(ip)
|
||||||
|
|
||||||
if ipAddr == nil {
|
if ipAddr == nil {
|
||||||
return false, fmt.Errorf("invalid ip address")
|
return false, errors.New("invalid IP address")
|
||||||
}
|
}
|
||||||
|
|
||||||
filter = strings.ReplaceAll(filter, "-", "/")
|
filter = strings.Replace(filter, "-", "/", -1)
|
||||||
|
|
||||||
if strings.Contains(filter, "/") {
|
if strings.Contains(filter, "/") {
|
||||||
_, cidr, err := net.ParseCIDR(filter)
|
_, cidr, err := net.ParseCIDR(filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("invalid cidr notation: %w", err)
|
return false, err
|
||||||
}
|
}
|
||||||
return cidr.Contains(ipAddr), nil
|
return cidr.Contains(ipAddr), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ipFilter := net.ParseIP(filter)
|
ipFilter := net.ParseIP(filter)
|
||||||
|
|
||||||
if ipFilter == nil {
|
if ipFilter == nil {
|
||||||
return false, fmt.Errorf("invalid ip address")
|
return false, errors.New("invalid IP address in filter")
|
||||||
}
|
}
|
||||||
|
|
||||||
if ipFilter.Equal(ipAddr) {
|
if ipFilter.Equal(ipAddr) {
|
||||||
@@ -76,29 +75,31 @@ func CheckIPFilter(filter string, ip string) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckFilter(filter string, input string) (bool, error) {
|
func CheckFilter(filter string, str string) bool {
|
||||||
if len(strings.TrimSpace(filter)) == 0 {
|
if len(strings.TrimSpace(filter)) == 0 {
|
||||||
return false, fmt.Errorf("filter is empty")
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(filter, "/") && strings.HasSuffix(filter, "/") {
|
if strings.HasPrefix(filter, "/") && strings.HasSuffix(filter, "/") {
|
||||||
re, err := regexp.Compile(filter[1 : len(filter)-1])
|
re, err := regexp.Compile(filter[1 : len(filter)-1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("invalid regex filter: %w", err)
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if re.MatchString(input) {
|
if re.MatchString(strings.TrimSpace(str)) {
|
||||||
return true, nil
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for item := range strings.SplitSeq(filter, ",") {
|
filterSplit := strings.Split(filter, ",")
|
||||||
if strings.TrimSpace(item) == input {
|
|
||||||
return true, nil
|
for _, item := range filterSplit {
|
||||||
|
if strings.TrimSpace(item) == strings.TrimSpace(str) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, nil
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateUUID(str string) string {
|
func GenerateUUID(str string) string {
|
||||||
|
|||||||
@@ -75,77 +75,66 @@ func TestEncodeBasicAuth(t *testing.T) {
|
|||||||
assert.Equal(t, expected, utils.EncodeBasicAuth(username, password))
|
assert.Equal(t, expected, utils.EncodeBasicAuth(username, password))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckIPFilter(t *testing.T) {
|
func TestFilterIP(t *testing.T) {
|
||||||
// Exact match IPv4
|
// Exact match IPv4
|
||||||
ok, err := utils.CheckIPFilter("10.10.0.1", "10.10.0.1")
|
ok, err := utils.FilterIP("10.10.0.1", "10.10.0.1")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, true, ok)
|
assert.Equal(t, true, ok)
|
||||||
|
|
||||||
// Non-match IPv4
|
// Non-match IPv4
|
||||||
ok, err = utils.CheckIPFilter("10.10.0.1", "10.10.0.2")
|
ok, err = utils.FilterIP("10.10.0.1", "10.10.0.2")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, false, ok)
|
assert.Equal(t, false, ok)
|
||||||
|
|
||||||
// CIDR match IPv4
|
// CIDR match IPv4
|
||||||
ok, err = utils.CheckIPFilter("10.10.0.0/24", "10.10.0.2")
|
ok, err = utils.FilterIP("10.10.0.0/24", "10.10.0.2")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, true, ok)
|
assert.Equal(t, true, ok)
|
||||||
|
|
||||||
// CIDR match IPv4 with '-' instead of '/'
|
// CIDR match IPv4 with '-' instead of '/'
|
||||||
ok, err = utils.CheckIPFilter("10.10.10.0-24", "10.10.10.5")
|
ok, err = utils.FilterIP("10.10.10.0-24", "10.10.10.5")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, true, ok)
|
assert.Equal(t, true, ok)
|
||||||
|
|
||||||
// CIDR non-match IPv4
|
// CIDR non-match IPv4
|
||||||
ok, err = utils.CheckIPFilter("10.10.0.0/24", "10.5.0.1")
|
ok, err = utils.FilterIP("10.10.0.0/24", "10.5.0.1")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, false, ok)
|
assert.Equal(t, false, ok)
|
||||||
|
|
||||||
// Invalid CIDR
|
// Invalid CIDR
|
||||||
ok, err = utils.CheckIPFilter("10.10.0.0/222", "10.0.0.1")
|
ok, err = utils.FilterIP("10.10.0.0/222", "10.0.0.1")
|
||||||
assert.ErrorContains(t, err, "invalid cidr notation: invalid CIDR address: 10.10.0.0/222")
|
assert.ErrorContains(t, err, "invalid CIDR address")
|
||||||
assert.Equal(t, false, ok)
|
assert.Equal(t, false, ok)
|
||||||
|
|
||||||
// Invalid IP in filter
|
// Invalid IP in filter
|
||||||
ok, err = utils.CheckIPFilter("invalid_ip", "10.5.5.5")
|
ok, err = utils.FilterIP("invalid_ip", "10.5.5.5")
|
||||||
assert.ErrorContains(t, err, "invalid ip address")
|
assert.ErrorContains(t, err, "invalid IP address in filter")
|
||||||
assert.Equal(t, false, ok)
|
assert.Equal(t, false, ok)
|
||||||
|
|
||||||
// Invalid IP to check
|
// Invalid IP to check
|
||||||
ok, err = utils.CheckIPFilter("10.10.10.10", "invalid_ip")
|
ok, err = utils.FilterIP("10.10.10.10", "invalid_ip")
|
||||||
assert.ErrorContains(t, err, "invalid ip address")
|
assert.ErrorContains(t, err, "invalid IP address")
|
||||||
assert.Equal(t, false, ok)
|
assert.Equal(t, false, ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckFilter(t *testing.T) {
|
func TestCheckFilter(t *testing.T) {
|
||||||
// Empty filter
|
// Empty filter
|
||||||
_, err := utils.CheckFilter("", "anystring")
|
assert.Equal(t, true, utils.CheckFilter("", "anystring"))
|
||||||
assert.ErrorContains(t, err, "filter is empty")
|
|
||||||
|
|
||||||
// Exact match
|
// Exact match
|
||||||
ok, err := utils.CheckFilter("hello", "hello")
|
assert.Equal(t, true, utils.CheckFilter("hello", "hello"))
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, true, ok)
|
|
||||||
|
|
||||||
// Regex match
|
// Regex match
|
||||||
ok, err = utils.CheckFilter("/^h.*o$/", "hello")
|
assert.Equal(t, true, utils.CheckFilter("/^h.*o$/", "hello"))
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, true, ok)
|
|
||||||
|
|
||||||
// Invalid regex
|
// Invalid regex
|
||||||
ok, err = utils.CheckFilter("/[unclosed/", "test")
|
assert.Equal(t, false, utils.CheckFilter("/[unclosed", "test"))
|
||||||
assert.ErrorContains(t, err, "invalid regex")
|
|
||||||
assert.Equal(t, false, ok)
|
|
||||||
|
|
||||||
// Comma-separated values
|
// Comma-separated values
|
||||||
ok, err = utils.CheckFilter("apple, banana, cherry", "banana")
|
assert.Equal(t, true, utils.CheckFilter("apple, banana, cherry", "banana"))
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, true, ok)
|
|
||||||
|
|
||||||
// No match
|
// No match
|
||||||
ok, err = utils.CheckFilter("apple, banana, cherry", "grape")
|
assert.Equal(t, false, utils.CheckFilter("apple, banana, cherry", "grape"))
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, false, ok)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateUUID(t *testing.T) {
|
func TestGenerateUUID(t *testing.T) {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
version: "2"
|
version: "2"
|
||||||
sql:
|
sql:
|
||||||
- engine: "sqlite"
|
- engine: "sqlite"
|
||||||
queries: "sql/sqlite/*_queries.sql"
|
queries: "sql/*_queries.sql"
|
||||||
schema: "sql/sqlite/*_schemas.sql"
|
schema: "sql/*_schemas.sql"
|
||||||
gen:
|
gen:
|
||||||
go:
|
go:
|
||||||
package: "sqlite"
|
package: "repository"
|
||||||
out: "internal/repository/sqlite"
|
out: "internal/repository"
|
||||||
rename:
|
rename:
|
||||||
uuid: "UUID"
|
uuid: "UUID"
|
||||||
oauth_groups: "OAuthGroups"
|
oauth_groups: "OAuthGroups"
|
||||||
|
|||||||
Reference in New Issue
Block a user