mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-31 21:01:29 +02:00
Compare commits
5 Commits
v0.76.0
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c82e04a0b2 | ||
|
|
234abd7a08 | ||
|
|
c1f0006012 | ||
|
|
cff49237b6 | ||
|
|
1bf54ddd8f |
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -3,8 +3,8 @@ updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 15
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 3
|
||||
groups:
|
||||
actions:
|
||||
patterns:
|
||||
@@ -22,9 +22,12 @@ updates:
|
||||
directories:
|
||||
- "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 15
|
||||
groups:
|
||||
golang-x-packages:
|
||||
patterns:
|
||||
- "golang.org/x/*"
|
||||
aws-sdk:
|
||||
patterns:
|
||||
- "github.com/aws/aws-sdk-go-v2/*"
|
||||
|
||||
72
SECURITY.md
72
SECURITY.md
@@ -1,12 +1,70 @@
|
||||
# Security Policy
|
||||
|
||||
NetBird's goal is to provide a secure network. If you find a vulnerability or bug, please report it by opening an issue [here](https://github.com/netbirdio/netbird/issues/new?assignees=&labels=&template=bug-issue-report.md&title=) or by contacting us by email.
|
||||
|
||||
There has yet to be an official bug bounty program for the NetBird project.
|
||||
|
||||
## Supported Versions
|
||||
- We currently support only the latest version
|
||||
NetBird's goal is to provide a secure network. The client runs as a privileged service on every machine it is installed on,
|
||||
so we take reports about it seriously and we publish what we fix.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security issues to `security@netbird.io`
|
||||
**Please do not open a public issue for a security vulnerability.** Public issues are visible to everyone, including before
|
||||
a fix is available.
|
||||
|
||||
Report security issues one of these two ways:
|
||||
|
||||
- **GitHub private vulnerability reporting** — [open a private report](https://github.com/netbirdio/netbird/security/advisories/new)
|
||||
on this repository. This is the preferred route: it keeps the discussion, the draft advisory, and the credit in one place.
|
||||
- **Email** — `security@netbird.io`.
|
||||
|
||||
If the finding affects NetBird Cloud or our hosted infrastructure rather than the open-source code, email us rather than
|
||||
filing a repository report.
|
||||
|
||||
### What to include
|
||||
|
||||
A report is easier to act on when it contains:
|
||||
|
||||
- The affected component (client, management, signal, relay, dashboard) and the version or commit you tested
|
||||
- The platform and configuration, where relevant — operating system, self-hosted or NetBird Cloud, container or host install
|
||||
- What an attacker needs before they can exploit it: network position, an account, local access, a specific privilege level
|
||||
- Steps to reproduce, and a proof of concept if you have one
|
||||
- The impact you believe it has
|
||||
|
||||
Partial reports are still welcome. If you are unsure whether something is a security issue, send it to `security@netbird.io`
|
||||
and let us make that call.
|
||||
|
||||
## What to expect from us
|
||||
|
||||
- **We acknowledge your report** and tell you whether we can reproduce it.
|
||||
- **We work with you on severity and scope.** If we assess it differently than you do, we will explain why rather than
|
||||
silently downgrade it.
|
||||
- **We fix and release**, then publish a [GitHub Security Advisory](https://github.com/netbirdio/netbird/security/advisories)
|
||||
naming the affected version range and the patched version.
|
||||
- **We credit reporters who want to be credited.** Tell us the name or handle you would like used, or that you would rather
|
||||
stay anonymous.
|
||||
- **We keep you in the loop** until the advisory is published.
|
||||
|
||||
We ask that you give us a reasonable opportunity to ship a fix before disclosing the issue publicly, and that you avoid
|
||||
accessing, modifying, or exfiltrating data belonging to other people while testing. Testing against your own installation
|
||||
or your own account is always fine.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We support the latest release. Security fixes ship in the next version rather than as backports to older releases, so
|
||||
upgrading to the current release is how you get them.
|
||||
|
||||
Release notifications are available by watching [releases](https://github.com/netbirdio/netbird/releases).
|
||||
|
||||
## Published advisories
|
||||
|
||||
Every vulnerability we fix is published as a GitHub Security Advisory on the
|
||||
[advisories page](https://github.com/netbirdio/netbird/security/advisories), including the affected version range, the
|
||||
patched version, and the reporter's credit. Advisories for the Go module are also distributed through the Go vulnerability
|
||||
database, so `govulncheck` will report them against your dependencies.
|
||||
|
||||
## Bug bounty
|
||||
|
||||
There is no official bug bounty program for the NetBird project. We credit reporters in advisories, and we are grateful for
|
||||
the work, but we cannot currently offer payment for reports.
|
||||
|
||||
## Non-security bugs
|
||||
|
||||
For bugs that are not security issues, please use the
|
||||
[issue tracker](https://github.com/netbirdio/netbird/discussions/new/choose).
|
||||
|
||||
@@ -76,6 +76,24 @@ type Client struct {
|
||||
connectClient *internal.ConnectClient
|
||||
config *profilemanager.Config
|
||||
cacheDir string
|
||||
|
||||
stateChangeMu sync.Mutex
|
||||
stateChangeSubID string
|
||||
eventSub *peer.EventSubscription
|
||||
// Closed to stop the watch goroutines from delivering buffered items to a
|
||||
// listener that has been removed or replaced. See stopStateChangeWatchLocked.
|
||||
stateChangeDone chan struct{}
|
||||
|
||||
// Latched "the server wants an interactive login": survives the engine
|
||||
// restarts that replace the run loop's context state. See Client.Status.
|
||||
// Guarded by loginRequiredMu together with loginCleared, which counts
|
||||
// clears so a stale observation cannot re-latch over one.
|
||||
loginRequiredMu sync.Mutex
|
||||
loginRequired bool
|
||||
loginCleared uint64
|
||||
|
||||
extendMu sync.Mutex
|
||||
extendCancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (c *Client) setState(cfg *profilemanager.Config, cacheDir string, cc *internal.ConnectClient) {
|
||||
@@ -149,11 +167,16 @@ func (c *Client) Run(platformFiles PlatformFiles, urlOpener URLOpener, isAndroid
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// todo do not throw error in case of cancelled context
|
||||
ctx = internal.CtxInitState(ctx)
|
||||
connectClient := internal.NewConnectClient(ctx, cfg, c.recorder)
|
||||
c.setState(cfg, cacheDir, connectClient)
|
||||
// This path runs the interactive SSO flow, so reaching here means the peer
|
||||
// is authenticated again — release the latch Status() reports from. Clear
|
||||
// only once the fresh connect client is installed: until then Status()
|
||||
// still reads the previous run's context state, which holds the NeedsLogin
|
||||
// that prompted this login, and would re-latch what was just cleared.
|
||||
c.clearLoginRequired()
|
||||
return connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, stateFile, cacheDir)
|
||||
}
|
||||
|
||||
|
||||
309
client/android/session.go
Normal file
309
client/android/session.go
Normal file
@@ -0,0 +1,309 @@
|
||||
//go:build android
|
||||
|
||||
package android
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
"github.com/netbirdio/netbird/client/internal/auth"
|
||||
"github.com/netbirdio/netbird/client/internal/auth/sessionwatch"
|
||||
"github.com/netbirdio/netbird/client/internal/peer"
|
||||
cProto "github.com/netbirdio/netbird/client/proto"
|
||||
)
|
||||
|
||||
// StateChangeListener receives client state notifications.
|
||||
//
|
||||
// OnStateChanged is a payload-free wake-up whenever the state snapshot
|
||||
// changed: connection state, the run-loop status label (e.g. NeedsLogin) or
|
||||
// the session deadline. It mirrors the daemon's SubscribeStatus stream
|
||||
// trigger — on each signal the consumer pulls the fresh values via
|
||||
// Status() / SessionExpiresAtUnix().
|
||||
//
|
||||
// OnSessionExpiring forwards the engine's session-expiry warnings, fired at
|
||||
// sessionwatch.WarningLead before the deadline and again at FinalWarningLead
|
||||
// (finalWarning true). The second one is suppressed when the user dismissed
|
||||
// the first via DismissSessionWarning. The daemon turns the same events into
|
||||
// its tray notification.
|
||||
type StateChangeListener interface {
|
||||
OnStateChanged()
|
||||
OnSessionExpiring(expiresAtUnix int64, leadMinutes int64, finalWarning bool)
|
||||
}
|
||||
|
||||
// Status returns the connect run-loop's status label — the same value the
|
||||
// desktop daemon serves in StatusResponse.Status. "NeedsLogin" means the
|
||||
// management server rejected the peer and an interactive login is required.
|
||||
//
|
||||
// The label is latched: the run loop keeps its status in a per-run context
|
||||
// state, which a restart replaces with a fresh Idle one, so an engine restart
|
||||
// (network change, always-on) would otherwise erase the fact that the peer
|
||||
// still needs to log in. Only a successful interactive login or extend clears
|
||||
// it — see clearLoginRequired.
|
||||
func (c *Client) Status() string {
|
||||
latched, generation := c.loginRequiredState()
|
||||
if latched {
|
||||
return string(internal.StatusNeedsLogin)
|
||||
}
|
||||
cc := c.getConnectClient()
|
||||
if cc == nil {
|
||||
return string(internal.StatusIdle)
|
||||
}
|
||||
status := cc.Status()
|
||||
if status == internal.StatusNeedsLogin {
|
||||
c.latchLoginRequired(generation)
|
||||
}
|
||||
return string(status)
|
||||
}
|
||||
|
||||
func (c *Client) loginRequiredState() (bool, uint64) {
|
||||
c.loginRequiredMu.Lock()
|
||||
defer c.loginRequiredMu.Unlock()
|
||||
return c.loginRequired, c.loginCleared
|
||||
}
|
||||
|
||||
// latchLoginRequired records a NeedsLogin observation, unless a clear landed
|
||||
// while the caller was reading the run loop's status: cc.Status() is read
|
||||
// outside the lock, so a login or extend completing in that window would
|
||||
// otherwise be undone by this stale observation, stranding the UI on
|
||||
// "login required" over a healthy session.
|
||||
func (c *Client) latchLoginRequired(observedGeneration uint64) {
|
||||
c.loginRequiredMu.Lock()
|
||||
defer c.loginRequiredMu.Unlock()
|
||||
if c.loginCleared != observedGeneration {
|
||||
return
|
||||
}
|
||||
c.loginRequired = true
|
||||
}
|
||||
|
||||
// clearLoginRequired releases the latch after a successful interactive login
|
||||
// or session extend, and invalidates any observation already in flight.
|
||||
func (c *Client) clearLoginRequired() {
|
||||
c.loginRequiredMu.Lock()
|
||||
defer c.loginRequiredMu.Unlock()
|
||||
c.loginRequired = false
|
||||
c.loginCleared++
|
||||
}
|
||||
|
||||
// SessionExpiresAtUnix returns the SSO session deadline as unix seconds, or 0
|
||||
// when no deadline is known (not SSO-registered, expiry disabled, or the
|
||||
// engine has not received one yet). A past value means the session expired.
|
||||
// Mirror of StatusResponse.sessionExpiresAt on the desktop daemon.
|
||||
func (c *Client) SessionExpiresAtUnix() int64 {
|
||||
deadline := c.recorder.GetSessionExpiresAt()
|
||||
if deadline.IsZero() {
|
||||
return 0
|
||||
}
|
||||
return deadline.Unix()
|
||||
}
|
||||
|
||||
// SetStateChangeListener registers the state notification listener.
|
||||
// Replaces any previously registered listener; remove it with
|
||||
// RemoveStateChangeListener.
|
||||
func (c *Client) SetStateChangeListener(listener StateChangeListener) {
|
||||
c.stateChangeMu.Lock()
|
||||
defer c.stateChangeMu.Unlock()
|
||||
c.stopStateChangeWatchLocked()
|
||||
if listener == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Both subscriptions are buffered (one pending tick, ten pending events),
|
||||
// so unsubscribing is not enough to stop callbacks: the loops would drain
|
||||
// what is already queued and deliver it to a listener the caller has
|
||||
// already removed or replaced. Gate every callback on this registration's
|
||||
// own signal, which is closed before unsubscribing.
|
||||
done := make(chan struct{})
|
||||
c.stateChangeDone = done
|
||||
|
||||
id, ch := c.recorder.SubscribeToStateChanges()
|
||||
c.stateChangeSubID = id
|
||||
// The channel is closed by UnsubscribeFromStateChanges, which ends the
|
||||
// goroutine. Ticks are coalesced (buffer of one), so a burst of changes
|
||||
// wakes the listener once.
|
||||
go func() {
|
||||
for range ch {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
listener.OnStateChanged()
|
||||
}
|
||||
}()
|
||||
|
||||
c.eventSub = c.recorder.SubscribeToEvents()
|
||||
go watchSessionWarnings(c.eventSub, listener, done)
|
||||
}
|
||||
|
||||
// RemoveStateChangeListener unregisters the state notification listener.
|
||||
func (c *Client) RemoveStateChangeListener() {
|
||||
c.stateChangeMu.Lock()
|
||||
defer c.stateChangeMu.Unlock()
|
||||
c.stopStateChangeWatchLocked()
|
||||
}
|
||||
|
||||
// DismissSessionWarning records the user's "Dismiss" on the first expiry
|
||||
// warning and suppresses the final one for the current deadline. A refreshed
|
||||
// deadline re-arms both. No-op while the engine is not running.
|
||||
func (c *Client) DismissSessionWarning() {
|
||||
cc := c.getConnectClient()
|
||||
if cc == nil {
|
||||
return
|
||||
}
|
||||
engine := cc.Engine()
|
||||
if engine == nil {
|
||||
return
|
||||
}
|
||||
engine.DismissSessionWarning()
|
||||
}
|
||||
|
||||
// ExtendAuthSession runs the interactive SSO flow to obtain a fresh JWT and
|
||||
// asks the management server to extend the session deadline. The tunnel is
|
||||
// untouched: no resync, no reconnect. Async; the result arrives on the
|
||||
// listener. Mirror of the daemon's RequestExtendAuthSession /
|
||||
// WaitExtendAuthSession RPC pair, with URLOpener playing the "UI opens the
|
||||
// browser" role.
|
||||
//
|
||||
// Only one flow may be in flight: the PKCE step binds a fixed loopback port,
|
||||
// so a second concurrent flow would fail on that bind. Call
|
||||
// CancelExtendAuthSession when the user abandons the browser.
|
||||
func (c *Client) ExtendAuthSession(urlOpener URLOpener, isAndroidTV bool, resultListener ErrListener) {
|
||||
ctx, err := c.beginExtend()
|
||||
if err != nil {
|
||||
resultListener.OnError(err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer c.endExtend()
|
||||
if err := c.extendAuthSession(ctx, urlOpener, isAndroidTV); err != nil {
|
||||
resultListener.OnError(err)
|
||||
return
|
||||
}
|
||||
resultListener.OnSuccess()
|
||||
}()
|
||||
}
|
||||
|
||||
// CancelExtendAuthSession aborts an in-flight ExtendAuthSession. The tunnel is
|
||||
// left alone — unlike the login flow, which cancels the whole client context
|
||||
// by stopping the engine. Without this the abandoned PKCE wait keeps its
|
||||
// loopback port for the full flow timeout and blocks every later attempt.
|
||||
// No-op when no flow is running.
|
||||
func (c *Client) CancelExtendAuthSession() {
|
||||
c.extendMu.Lock()
|
||||
defer c.extendMu.Unlock()
|
||||
if c.extendCancel != nil {
|
||||
c.extendCancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) stopStateChangeWatchLocked() {
|
||||
// Signal first, unsubscribe second: closing the channels only stops new
|
||||
// items, and the loops would still hand whatever is buffered to a listener
|
||||
// that is no longer registered.
|
||||
if c.stateChangeDone != nil {
|
||||
close(c.stateChangeDone)
|
||||
c.stateChangeDone = nil
|
||||
}
|
||||
if c.stateChangeSubID != "" {
|
||||
c.recorder.UnsubscribeFromStateChanges(c.stateChangeSubID)
|
||||
c.stateChangeSubID = ""
|
||||
}
|
||||
if c.eventSub != nil {
|
||||
// Closes the channel, which ends watchSessionWarnings.
|
||||
c.recorder.UnsubscribeFromEvents(c.eventSub)
|
||||
c.eventSub = nil
|
||||
}
|
||||
}
|
||||
|
||||
// watchSessionWarnings forwards the engine's session-expiry warnings to the
|
||||
// listener. The event stream also carries unrelated traffic — network-map
|
||||
// updates on every sync, DNS and route errors — so everything but an
|
||||
// AUTHENTICATION event carrying the session-warning marker is dropped. Exits
|
||||
// when the subscription is closed by UnsubscribeFromEvents, or earlier when
|
||||
// done is closed — the stream buffers up to ten events, and a deregistered
|
||||
// listener must not receive the ones already queued.
|
||||
func watchSessionWarnings(sub *peer.EventSubscription, listener StateChangeListener, done <-chan struct{}) {
|
||||
for ev := range sub.Events() {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
if ev.GetCategory() != cProto.SystemEvent_AUTHENTICATION {
|
||||
continue
|
||||
}
|
||||
meta := ev.GetMetadata()
|
||||
if meta[sessionwatch.MetaSessionWarning] != "true" {
|
||||
// Other AUTHENTICATION events exist (e.g. a deadline rejected as
|
||||
// out of range); they carry no warning marker.
|
||||
continue
|
||||
}
|
||||
deadline, err := sessionwatch.ParseExpiresAt(meta[sessionwatch.MetaSessionExpiresAt])
|
||||
if err != nil {
|
||||
log.Warnf("session warning event with unparsable deadline: %v", err)
|
||||
continue
|
||||
}
|
||||
lead, err := sessionwatch.ParseLeadMinutes(meta[sessionwatch.MetaSessionLeadMinutes])
|
||||
if err != nil {
|
||||
// Informational only — the deadline above is what drives the UI.
|
||||
lead = 0
|
||||
}
|
||||
listener.OnSessionExpiring(deadline.Unix(), int64(lead),
|
||||
meta[sessionwatch.MetaSessionFinal] == "true")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) beginExtend() (context.Context, error) {
|
||||
c.extendMu.Lock()
|
||||
defer c.extendMu.Unlock()
|
||||
if c.extendCancel != nil {
|
||||
return nil, fmt.Errorf("session extend already in progress")
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c.extendCancel = cancel
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (c *Client) endExtend() {
|
||||
c.extendMu.Lock()
|
||||
defer c.extendMu.Unlock()
|
||||
if c.extendCancel != nil {
|
||||
c.extendCancel()
|
||||
c.extendCancel = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) extendAuthSession(ctx context.Context, urlOpener URLOpener, isAndroidTV bool) error {
|
||||
cfg, _, cc := c.stateSnapshot()
|
||||
if cfg == nil || cc == nil {
|
||||
return fmt.Errorf("engine is not running")
|
||||
}
|
||||
engine := cc.Engine()
|
||||
if engine == nil {
|
||||
return fmt.Errorf("engine is not initialized")
|
||||
}
|
||||
|
||||
authClient, err := auth.NewAuth(ctx, cfg.PrivateKey, cfg.ManagementURL, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create auth client: %v", err)
|
||||
}
|
||||
defer authClient.Close()
|
||||
|
||||
a := &Auth{ctx: ctx, config: cfg}
|
||||
tokenInfo, err := a.foregroundGetTokenInfo(authClient, urlOpener, isAndroidTV)
|
||||
if err != nil {
|
||||
return fmt.Errorf("interactive sso login failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err := engine.ExtendAuthSession(ctx, tokenInfo.GetTokenToUse()); err != nil {
|
||||
return err
|
||||
}
|
||||
c.clearLoginRequired()
|
||||
|
||||
go urlOpener.OnLoginSuccess()
|
||||
return nil
|
||||
}
|
||||
@@ -29,8 +29,13 @@ cask "{{ $projectName }}" do
|
||||
end
|
||||
|
||||
uninstall_preflight do
|
||||
system_command "#{appdir}/Netbird UI.app/uninstaller.sh",
|
||||
sudo: false
|
||||
system_command "/bin/sh",
|
||||
args: ["-c", <<~CMD],
|
||||
launchctl bootout system/netbird 2>/dev/null || \
|
||||
launchctl unload /Library/LaunchDaemons/netbird.plist 2>/dev/null || true
|
||||
rm -f /Library/LaunchDaemons/netbird.plist
|
||||
CMD
|
||||
sudo: true
|
||||
end
|
||||
|
||||
name "Netbird UI"
|
||||
|
||||
2
go.mod
2
go.mod
@@ -83,7 +83,7 @@ require (
|
||||
github.com/moby/moby/api v1.54.1
|
||||
github.com/netbirdio/management-integrations/integrations v0.0.0-20260416123949-2355d972be42
|
||||
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45
|
||||
github.com/oapi-codegen/runtime v1.1.2
|
||||
github.com/oapi-codegen/runtime v1.6.0
|
||||
github.com/okta/okta-sdk-golang/v2 v2.18.0
|
||||
github.com/ory/dockertest/v4 v4.0.0
|
||||
github.com/oschwald/maxminddb-golang v1.12.0
|
||||
|
||||
6
go.sum
6
go.sum
@@ -496,8 +496,10 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
|
||||
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
|
||||
github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI=
|
||||
github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
||||
github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs=
|
||||
github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY=
|
||||
github.com/oapi-codegen/runtime v1.6.0 h1:7Xx+GlueD6nRuyKoCPzL434Jfi3BetbiJOrzCHp/VPU=
|
||||
github.com/oapi-codegen/runtime v1.6.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/okta/okta-sdk-golang/v2 v2.18.0 h1:cfDasMb7CShbZvOrF6n+DnLevWwiHgedWMGJ8M8xKDc=
|
||||
|
||||
Reference in New Issue
Block a user