Add Tailnet UI auth test flow
This commit is contained in:
parent
0c660acd1e
commit
75bcfaf655
13 changed files with 872 additions and 4 deletions
232
Apple/AppUITests/BurrowUITests.swift
Normal file
232
Apple/AppUITests/BurrowUITests.swift
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class BurrowTailnetLoginUITests: XCTestCase {
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTailnetLoginThroughAuthentikWebSession() throws {
|
||||||
|
let email = try requiredEnvironment("BURROW_UI_TEST_EMAIL")
|
||||||
|
let username = ProcessInfo.processInfo.environment["BURROW_UI_TEST_USERNAME"] ?? email
|
||||||
|
let password = try requiredEnvironment("BURROW_UI_TEST_PASSWORD")
|
||||||
|
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
let tailnetButton = app.buttons["quick-add-tailnet"]
|
||||||
|
XCTAssertTrue(tailnetButton.waitForExistence(timeout: 15), "Tailnet add button did not appear")
|
||||||
|
tailnetButton.tap()
|
||||||
|
|
||||||
|
let discoveryField = app.textFields["tailnet-discovery-email"]
|
||||||
|
XCTAssertTrue(discoveryField.waitForExistence(timeout: 10), "Tailnet discovery email field did not appear")
|
||||||
|
replaceText(in: discoveryField, with: email)
|
||||||
|
|
||||||
|
let findServerButton = app.buttons["tailnet-find-server"]
|
||||||
|
XCTAssertTrue(findServerButton.waitForExistence(timeout: 5), "Find Server button did not appear")
|
||||||
|
findServerButton.tap()
|
||||||
|
|
||||||
|
let discoveryCard = app.otherElements["tailnet-discovery-card"]
|
||||||
|
XCTAssertTrue(discoveryCard.waitForExistence(timeout: 20), "Tailnet discovery result did not appear")
|
||||||
|
|
||||||
|
let authorityField = app.textFields["tailnet-authority"]
|
||||||
|
XCTAssertTrue(authorityField.waitForExistence(timeout: 10), "Tailnet authority field did not appear")
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForFieldValue(authorityField, containing: "ts.burrow.net", timeout: 20),
|
||||||
|
"Tailnet authority was not populated from discovery"
|
||||||
|
)
|
||||||
|
|
||||||
|
let probeButton = app.buttons["tailnet-check-connection"]
|
||||||
|
XCTAssertTrue(probeButton.waitForExistence(timeout: 5), "Check Connection button did not appear")
|
||||||
|
probeButton.tap()
|
||||||
|
|
||||||
|
let probeCard = app.otherElements["tailnet-authority-probe-card"]
|
||||||
|
XCTAssertTrue(probeCard.waitForExistence(timeout: 20), "Tailnet connection probe did not complete")
|
||||||
|
|
||||||
|
let signInButton = app.buttons["tailnet-start-sign-in"]
|
||||||
|
XCTAssertTrue(signInButton.waitForExistence(timeout: 10), "Tailnet sign-in button did not appear")
|
||||||
|
signInButton.tap()
|
||||||
|
|
||||||
|
acceptAuthenticationPromptIfNeeded(in: app)
|
||||||
|
|
||||||
|
let webSession = webAuthenticationSession()
|
||||||
|
XCTAssertTrue(webSession.waitForExistence(timeout: 20), "Safari authentication session did not appear")
|
||||||
|
|
||||||
|
signIntoAuthentik(in: webSession, username: username, password: password)
|
||||||
|
|
||||||
|
app.activate()
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForButtonLabel(app.buttons["tailnet-start-sign-in"], equals: "Signed In", timeout: 60),
|
||||||
|
"Tailnet sign-in never reached the running state"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func acceptAuthenticationPromptIfNeeded(in app: XCUIApplication) {
|
||||||
|
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
|
||||||
|
let promptCandidates = [
|
||||||
|
springboard.buttons["Continue"],
|
||||||
|
springboard.buttons["Allow"],
|
||||||
|
app.buttons["Continue"],
|
||||||
|
app.buttons["Allow"],
|
||||||
|
]
|
||||||
|
|
||||||
|
for button in promptCandidates where button.waitForExistence(timeout: 3) {
|
||||||
|
button.tap()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func webAuthenticationSession() -> XCUIApplication {
|
||||||
|
let safariViewService = XCUIApplication(bundleIdentifier: "com.apple.SafariViewService")
|
||||||
|
if safariViewService.waitForExistence(timeout: 5) {
|
||||||
|
return safariViewService
|
||||||
|
}
|
||||||
|
|
||||||
|
let safari = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari")
|
||||||
|
_ = safari.waitForExistence(timeout: 5)
|
||||||
|
return safari
|
||||||
|
}
|
||||||
|
|
||||||
|
private func signIntoAuthentik(in webSession: XCUIApplication, username: String, password: String) {
|
||||||
|
let usernameField = firstExistingElement(
|
||||||
|
in: webSession,
|
||||||
|
queries: [
|
||||||
|
{ $0.textFields["Username"] },
|
||||||
|
{ $0.textFields["Email or Username"] },
|
||||||
|
{ $0.textFields["Email address"] },
|
||||||
|
{ $0.textFields["Email"] },
|
||||||
|
{ $0.webViews.textFields["Username"] },
|
||||||
|
{ $0.webViews.textFields["Email or Username"] },
|
||||||
|
{ $0.descendants(matching: .textField).firstMatch },
|
||||||
|
],
|
||||||
|
timeout: 25
|
||||||
|
)
|
||||||
|
XCTAssertTrue(usernameField.exists, "Authentik username field did not appear")
|
||||||
|
replaceText(in: usernameField, with: username)
|
||||||
|
|
||||||
|
let immediatePasswordField = firstExistingSecureField(in: webSession, timeout: 2)
|
||||||
|
if immediatePasswordField.exists {
|
||||||
|
replaceSecureText(in: immediatePasswordField, with: password)
|
||||||
|
tapFirstExistingButton(
|
||||||
|
in: webSession,
|
||||||
|
titles: ["Continue", "Sign In", "Log in", "Login"],
|
||||||
|
timeout: 5
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapFirstExistingButton(
|
||||||
|
in: webSession,
|
||||||
|
titles: ["Continue", "Next", "Sign In", "Log in", "Login"],
|
||||||
|
timeout: 5
|
||||||
|
)
|
||||||
|
|
||||||
|
let passwordField = firstExistingSecureField(in: webSession, timeout: 20)
|
||||||
|
XCTAssertTrue(passwordField.exists, "Authentik password field did not appear")
|
||||||
|
replaceSecureText(in: passwordField, with: password)
|
||||||
|
tapFirstExistingButton(
|
||||||
|
in: webSession,
|
||||||
|
titles: ["Continue", "Sign In", "Log in", "Login"],
|
||||||
|
timeout: 5
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func firstExistingSecureField(in app: XCUIApplication, timeout: TimeInterval) -> XCUIElement {
|
||||||
|
let candidates = [
|
||||||
|
app.secureTextFields["Password"],
|
||||||
|
app.secureTextFields["Password or Token"],
|
||||||
|
app.webViews.secureTextFields["Password"],
|
||||||
|
app.webViews.secureTextFields["Password or Token"],
|
||||||
|
app.descendants(matching: .secureTextField).firstMatch,
|
||||||
|
]
|
||||||
|
|
||||||
|
return firstExistingElement(from: candidates, timeout: timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func tapFirstExistingButton(
|
||||||
|
in app: XCUIApplication,
|
||||||
|
titles: [String],
|
||||||
|
timeout: TimeInterval
|
||||||
|
) {
|
||||||
|
let candidates = titles.flatMap { title in
|
||||||
|
[
|
||||||
|
app.buttons[title],
|
||||||
|
app.webViews.buttons[title],
|
||||||
|
]
|
||||||
|
} + [app.descendants(matching: .button).firstMatch]
|
||||||
|
|
||||||
|
let button = firstExistingElement(from: candidates, timeout: timeout)
|
||||||
|
XCTAssertTrue(button.exists, "Expected one of \(titles.joined(separator: ", ")) to appear")
|
||||||
|
button.tap()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requiredEnvironment(_ key: String) throws -> String {
|
||||||
|
guard let value = ProcessInfo.processInfo.environment[key],
|
||||||
|
!value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
|
else {
|
||||||
|
throw XCTSkip("Missing required UI test environment variable \(key)")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
private func waitForFieldValue(
|
||||||
|
_ field: XCUIElement,
|
||||||
|
containing substring: String,
|
||||||
|
timeout: TimeInterval
|
||||||
|
) -> Bool {
|
||||||
|
let predicate = NSPredicate(format: "value CONTAINS %@", substring)
|
||||||
|
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: field)
|
||||||
|
return XCTWaiter.wait(for: [expectation], timeout: timeout) == .completed
|
||||||
|
}
|
||||||
|
|
||||||
|
private func waitForButtonLabel(
|
||||||
|
_ button: XCUIElement,
|
||||||
|
equals expected: String,
|
||||||
|
timeout: TimeInterval
|
||||||
|
) -> Bool {
|
||||||
|
let predicate = NSPredicate(format: "label == %@", expected)
|
||||||
|
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: button)
|
||||||
|
return XCTWaiter.wait(for: [expectation], timeout: timeout) == .completed
|
||||||
|
}
|
||||||
|
|
||||||
|
private func firstExistingElement(
|
||||||
|
in app: XCUIApplication,
|
||||||
|
queries: [(XCUIApplication) -> XCUIElement],
|
||||||
|
timeout: TimeInterval
|
||||||
|
) -> XCUIElement {
|
||||||
|
firstExistingElement(from: queries.map { $0(app) }, timeout: timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func firstExistingElement(from candidates: [XCUIElement], timeout: TimeInterval) -> XCUIElement {
|
||||||
|
let deadline = Date().addingTimeInterval(timeout)
|
||||||
|
repeat {
|
||||||
|
for candidate in candidates where candidate.exists {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
|
||||||
|
} while Date() < deadline
|
||||||
|
|
||||||
|
return candidates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func replaceText(in element: XCUIElement, with value: String) {
|
||||||
|
element.tap()
|
||||||
|
clearText(in: element)
|
||||||
|
element.typeText(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func replaceSecureText(in element: XCUIElement, with value: String) {
|
||||||
|
element.tap()
|
||||||
|
clearText(in: element)
|
||||||
|
element.typeText(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clearText(in element: XCUIElement) {
|
||||||
|
guard let currentValue = element.value as? String, !currentValue.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let deleteSequence = String(repeating: XCUIKeyboardKey.delete.rawValue, count: currentValue.count)
|
||||||
|
element.typeText(deleteSequence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00AA8962A4669BC005C8102 /* AppDelegate.swift */; };
|
D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00AA8962A4669BC005C8102 /* AppDelegate.swift */; };
|
||||||
|
D11000012F70000100112233 /* BurrowUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11000042F70000100112233 /* BurrowUITests.swift */; };
|
||||||
D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */; };
|
D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */; };
|
||||||
D020F65D29E4A697002790F6 /* BurrowNetworkExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
D020F65D29E4A697002790F6 /* BurrowNetworkExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
D03383AD2C8E67E300F7C44E /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = D078F7E22C8DA375008A8CEC /* SwiftProtobuf */; };
|
D03383AD2C8E67E300F7C44E /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = D078F7E22C8DA375008A8CEC /* SwiftProtobuf */; };
|
||||||
|
|
@ -49,6 +50,13 @@
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
D11000022F70000100112233 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = D05B9F7129E39EEC008CB1F9;
|
||||||
|
remoteInfo = App;
|
||||||
|
};
|
||||||
D020F65B29E4A697002790F6 /* PBXContainerItemProxy */ = {
|
D020F65B29E4A697002790F6 /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */;
|
containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */;
|
||||||
|
|
@ -130,6 +138,9 @@
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
D00117422B30348D00D87C25 /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Configuration.xcconfig; sourceTree = "<group>"; };
|
D00117422B30348D00D87C25 /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Configuration.xcconfig; sourceTree = "<group>"; };
|
||||||
D00AA8962A4669BC005C8102 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
D00AA8962A4669BC005C8102 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
D11000032F70000100112233 /* BurrowUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BurrowUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
D11000042F70000100112233 /* BurrowUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowUITests.swift; sourceTree = "<group>"; };
|
||||||
|
D11000052F70000100112233 /* UITests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UITests.xcconfig; sourceTree = "<group>"; };
|
||||||
D020F63D29E4A1FF002790F6 /* Identity.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Identity.xcconfig; sourceTree = "<group>"; };
|
D020F63D29E4A1FF002790F6 /* Identity.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Identity.xcconfig; sourceTree = "<group>"; };
|
||||||
D020F64029E4A1FF002790F6 /* Compiler.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Compiler.xcconfig; sourceTree = "<group>"; };
|
D020F64029E4A1FF002790F6 /* Compiler.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Compiler.xcconfig; sourceTree = "<group>"; };
|
||||||
D020F64229E4A1FF002790F6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D020F64229E4A1FF002790F6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
|
@ -182,6 +193,13 @@
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
D11000062F70000100112233 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
D020F65029E4A697002790F6 /* Frameworks */ = {
|
D020F65029E4A697002790F6 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
|
@ -243,6 +261,7 @@
|
||||||
D0D4E4F72C8D941D007F820A /* Framework.xcconfig */,
|
D0D4E4F72C8D941D007F820A /* Framework.xcconfig */,
|
||||||
D020F64029E4A1FF002790F6 /* Compiler.xcconfig */,
|
D020F64029E4A1FF002790F6 /* Compiler.xcconfig */,
|
||||||
D0D4E4F62C8D932D007F820A /* Debug.xcconfig */,
|
D0D4E4F62C8D932D007F820A /* Debug.xcconfig */,
|
||||||
|
D11000052F70000100112233 /* UITests.xcconfig */,
|
||||||
D04A3E1D2BAF465F0043EC85 /* Version.xcconfig */,
|
D04A3E1D2BAF465F0043EC85 /* Version.xcconfig */,
|
||||||
D020F64229E4A1FF002790F6 /* Info.plist */,
|
D020F64229E4A1FF002790F6 /* Info.plist */,
|
||||||
D0D4E5912C8D9D0A007F820A /* Constants */,
|
D0D4E5912C8D9D0A007F820A /* Constants */,
|
||||||
|
|
@ -268,6 +287,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D05B9F7429E39EEC008CB1F9 /* App */,
|
D05B9F7429E39EEC008CB1F9 /* App */,
|
||||||
|
D11000072F70000100112233 /* AppUITests */,
|
||||||
D020F65629E4A697002790F6 /* NetworkExtension */,
|
D020F65629E4A697002790F6 /* NetworkExtension */,
|
||||||
D0D4E49C2C8D921A007F820A /* Core */,
|
D0D4E49C2C8D921A007F820A /* Core */,
|
||||||
D0D4E4AD2C8D921A007F820A /* UI */,
|
D0D4E4AD2C8D921A007F820A /* UI */,
|
||||||
|
|
@ -281,6 +301,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D05B9F7229E39EEC008CB1F9 /* Burrow.app */,
|
D05B9F7229E39EEC008CB1F9 /* Burrow.app */,
|
||||||
|
D11000032F70000100112233 /* BurrowUITests.xctest */,
|
||||||
D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */,
|
D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */,
|
||||||
D0BCC6032A09535900AD070D /* libburrow.a */,
|
D0BCC6032A09535900AD070D /* libburrow.a */,
|
||||||
D0D4E5312C8D996F007F820A /* BurrowCore.framework */,
|
D0D4E5312C8D996F007F820A /* BurrowCore.framework */,
|
||||||
|
|
@ -303,6 +324,14 @@
|
||||||
path = App;
|
path = App;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D11000072F70000100112233 /* AppUITests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D11000042F70000100112233 /* BurrowUITests.swift */,
|
||||||
|
);
|
||||||
|
path = AppUITests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D0B98FD729FDDB57004E7149 /* libburrow */ = {
|
D0B98FD729FDDB57004E7149 /* libburrow */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -375,6 +404,24 @@
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
D11000082F70000100112233 /* BurrowUITests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = D110000E2F70000100112233 /* Build configuration list for PBXNativeTarget "BurrowUITests" */;
|
||||||
|
buildPhases = (
|
||||||
|
D110000A2F70000100112233 /* Sources */,
|
||||||
|
D11000062F70000100112233 /* Frameworks */,
|
||||||
|
D11000092F70000100112233 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
D110000B2F70000100112233 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = BurrowUITests;
|
||||||
|
productName = BurrowUITests;
|
||||||
|
productReference = D11000032F70000100112233 /* BurrowUITests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.ui-testing";
|
||||||
|
};
|
||||||
D020F65229E4A697002790F6 /* NetworkExtension */ = {
|
D020F65229E4A697002790F6 /* NetworkExtension */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = D020F65E29E4A697002790F6 /* Build configuration list for PBXNativeTarget "NetworkExtension" */;
|
buildConfigurationList = D020F65E29E4A697002790F6 /* Build configuration list for PBXNativeTarget "NetworkExtension" */;
|
||||||
|
|
@ -490,6 +537,10 @@
|
||||||
LastSwiftUpdateCheck = 1600;
|
LastSwiftUpdateCheck = 1600;
|
||||||
LastUpgradeCheck = 1520;
|
LastUpgradeCheck = 1520;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
|
D11000082F70000100112233 = {
|
||||||
|
CreatedOnToolsVersion = 16.0;
|
||||||
|
TestTargetID = D05B9F7129E39EEC008CB1F9;
|
||||||
|
};
|
||||||
D020F65229E4A697002790F6 = {
|
D020F65229E4A697002790F6 = {
|
||||||
CreatedOnToolsVersion = 14.3;
|
CreatedOnToolsVersion = 14.3;
|
||||||
};
|
};
|
||||||
|
|
@ -522,6 +573,7 @@
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
D05B9F7129E39EEC008CB1F9 /* App */,
|
D05B9F7129E39EEC008CB1F9 /* App */,
|
||||||
|
D11000082F70000100112233 /* BurrowUITests */,
|
||||||
D020F65229E4A697002790F6 /* NetworkExtension */,
|
D020F65229E4A697002790F6 /* NetworkExtension */,
|
||||||
D0D4E5502C8D9BF2007F820A /* UI */,
|
D0D4E5502C8D9BF2007F820A /* UI */,
|
||||||
D0D4E5302C8D996F007F820A /* Core */,
|
D0D4E5302C8D996F007F820A /* Core */,
|
||||||
|
|
@ -531,6 +583,13 @@
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
D11000092F70000100112233 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
D05B9F7029E39EEC008CB1F9 /* Resources */ = {
|
D05B9F7029E39EEC008CB1F9 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
|
@ -594,6 +653,14 @@
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
D110000A2F70000100112233 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
D11000012F70000100112233 /* BurrowUITests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
D020F64F29E4A697002790F6 /* Sources */ = {
|
D020F64F29E4A697002790F6 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
|
@ -652,6 +719,11 @@
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
|
D110000B2F70000100112233 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = D05B9F7129E39EEC008CB1F9 /* App */;
|
||||||
|
targetProxy = D11000022F70000100112233 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
D020F65C29E4A697002790F6 /* PBXTargetDependency */ = {
|
D020F65C29E4A697002790F6 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = D020F65229E4A697002790F6 /* NetworkExtension */;
|
target = D020F65229E4A697002790F6 /* NetworkExtension */;
|
||||||
|
|
@ -694,6 +766,20 @@
|
||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
|
D110000C2F70000100112233 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = D11000052F70000100112233 /* UITests.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
D110000D2F70000100112233 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = D11000052F70000100112233 /* UITests.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
D020F65F29E4A697002790F6 /* Debug */ = {
|
D020F65F29E4A697002790F6 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = D020F66229E4A6E5002790F6 /* NetworkExtension.xcconfig */;
|
baseConfigurationReference = D020F66229E4A6E5002790F6 /* NetworkExtension.xcconfig */;
|
||||||
|
|
@ -781,6 +867,15 @@
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
D110000E2F70000100112233 /* Build configuration list for PBXNativeTarget "BurrowUITests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
D110000C2F70000100112233 /* Debug */,
|
||||||
|
D110000D2F70000100112233 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
D020F65E29E4A697002790F6 /* Build configuration list for PBXNativeTarget "NetworkExtension" */ = {
|
D020F65E29E4A697002790F6 /* Build configuration list for PBXNativeTarget "NetworkExtension" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,20 @@
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
shouldAutocreateTestPlan = "YES">
|
shouldAutocreateTestPlan = "NO">
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D11000082F70000100112233"
|
||||||
|
BuildableName = "BurrowUITests.xctest"
|
||||||
|
BlueprintName = "BurrowUITests"
|
||||||
|
ReferencedContainer = "container:Burrow.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
|
|
|
||||||
14
Apple/Configuration/UITests.xcconfig
Normal file
14
Apple/Configuration/UITests.xcconfig
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#include "Compiler.xcconfig"
|
||||||
|
|
||||||
|
SUPPORTED_PLATFORMS = iphonesimulator iphoneos
|
||||||
|
TARGETED_DEVICE_FAMILY[sdk=iphone*] = 1,2
|
||||||
|
|
||||||
|
PRODUCT_NAME = $(TARGET_NAME)
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).uitests
|
||||||
|
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = NO
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO
|
||||||
|
|
||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks
|
||||||
|
TEST_TARGET_NAME = App
|
||||||
|
|
@ -276,6 +276,7 @@ private struct QuickAddButton: View {
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, minHeight: 64, alignment: .leading)
|
.frame(maxWidth: .infinity, minHeight: 64, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("quick-add-\(sheet.rawValue)")
|
||||||
.buttonStyle(.floating(color: sheet.quickActionColor, cornerRadius: 18))
|
.buttonStyle(.floating(color: sheet.quickActionColor, cornerRadius: 18))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -485,6 +486,7 @@ private struct ConfigurationSheetView: View {
|
||||||
.burrowEmailField()
|
.burrowEmailField()
|
||||||
.burrowLoginField()
|
.burrowLoginField()
|
||||||
.autocorrectionDisabled()
|
.autocorrectionDisabled()
|
||||||
|
.accessibilityIdentifier("tailnet-discovery-email")
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
discoverTailnetAuthority()
|
discoverTailnetAuthority()
|
||||||
|
|
@ -497,6 +499,7 @@ private struct ConfigurationSheetView: View {
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
.disabled(isDiscoveringTailnet || normalizedOptional(draft.discoveryEmail) == nil)
|
.disabled(isDiscoveringTailnet || normalizedOptional(draft.discoveryEmail) == nil)
|
||||||
|
.accessibilityIdentifier("tailnet-find-server")
|
||||||
|
|
||||||
if let discoveryStatus {
|
if let discoveryStatus {
|
||||||
tailnetDiscoveryCard(status: discoveryStatus, failure: nil)
|
tailnetDiscoveryCard(status: discoveryStatus, failure: nil)
|
||||||
|
|
@ -507,6 +510,7 @@ private struct ConfigurationSheetView: View {
|
||||||
TextField("Authority URL", text: $draft.authority)
|
TextField("Authority URL", text: $draft.authority)
|
||||||
.burrowLoginField()
|
.burrowLoginField()
|
||||||
.autocorrectionDisabled()
|
.autocorrectionDisabled()
|
||||||
|
.accessibilityIdentifier("tailnet-authority")
|
||||||
|
|
||||||
Text("Use the managed Tailnet authority or enter a custom Tailnet control server.")
|
Text("Use the managed Tailnet authority or enter a custom Tailnet control server.")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
|
|
@ -523,6 +527,7 @@ private struct ConfigurationSheetView: View {
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
.disabled(isProbingAuthority || normalizedOptional(draft.authority) == nil)
|
.disabled(isProbingAuthority || normalizedOptional(draft.authority) == nil)
|
||||||
|
.accessibilityIdentifier("tailnet-check-connection")
|
||||||
|
|
||||||
if let authorityProbeStatus {
|
if let authorityProbeStatus {
|
||||||
tailnetAuthorityProbeCard(status: authorityProbeStatus, failure: nil)
|
tailnetAuthorityProbeCard(status: authorityProbeStatus, failure: nil)
|
||||||
|
|
@ -533,6 +538,7 @@ private struct ConfigurationSheetView: View {
|
||||||
TextField("Tailnet", text: $draft.tailnet)
|
TextField("Tailnet", text: $draft.tailnet)
|
||||||
.burrowLoginField()
|
.burrowLoginField()
|
||||||
.autocorrectionDisabled()
|
.autocorrectionDisabled()
|
||||||
|
.accessibilityIdentifier("tailnet-name")
|
||||||
}
|
}
|
||||||
|
|
||||||
Section("Authentication") {
|
Section("Authentication") {
|
||||||
|
|
@ -555,6 +561,7 @@ private struct ConfigurationSheetView: View {
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
.disabled(isStartingTailnetLogin || normalizedOptional(draft.authority) == nil)
|
.disabled(isStartingTailnetLogin || normalizedOptional(draft.authority) == nil)
|
||||||
|
.accessibilityIdentifier("tailnet-start-sign-in")
|
||||||
|
|
||||||
if let tailnetLoginStatus {
|
if let tailnetLoginStatus {
|
||||||
tailnetLoginCard(status: tailnetLoginStatus, failure: nil)
|
tailnetLoginCard(status: tailnetLoginStatus, failure: nil)
|
||||||
|
|
@ -673,6 +680,7 @@ private struct ConfigurationSheetView: View {
|
||||||
RoundedRectangle(cornerRadius: 16)
|
RoundedRectangle(cornerRadius: 16)
|
||||||
.fill(.thinMaterial)
|
.fill(.thinMaterial)
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("tailnet-authority-probe-card")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func tailnetDiscoveryCard(
|
private func tailnetDiscoveryCard(
|
||||||
|
|
@ -711,6 +719,7 @@ private struct ConfigurationSheetView: View {
|
||||||
RoundedRectangle(cornerRadius: 16)
|
RoundedRectangle(cornerRadius: 16)
|
||||||
.fill(.thinMaterial)
|
.fill(.thinMaterial)
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("tailnet-discovery-card")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func tailnetLoginCard(
|
private func tailnetLoginCard(
|
||||||
|
|
@ -757,6 +766,7 @@ private struct ConfigurationSheetView: View {
|
||||||
RoundedRectangle(cornerRadius: 16)
|
RoundedRectangle(cornerRadius: 16)
|
||||||
.fill(.thinMaterial)
|
.fill(.thinMaterial)
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("tailnet-login-card")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func summaryBadge(_ label: String) -> some View {
|
private func summaryBadge(_ label: String) -> some View {
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ lookup_user_pk() {
|
||||||
|
|
||||||
ensure_user() {
|
ensure_user() {
|
||||||
local user_spec="$1"
|
local user_spec="$1"
|
||||||
local username name email is_admin groups_json effective_groups_json group_name
|
local username name email is_admin groups_json password_file effective_groups_json group_name
|
||||||
local group_pks_json payload user_pk
|
local group_pks_json payload user_pk
|
||||||
|
|
||||||
username="$(printf '%s\n' "$user_spec" | jq -r '.username')"
|
username="$(printf '%s\n' "$user_spec" | jq -r '.username')"
|
||||||
|
|
@ -124,6 +124,7 @@ ensure_user() {
|
||||||
email="$(printf '%s\n' "$user_spec" | jq -r '.email')"
|
email="$(printf '%s\n' "$user_spec" | jq -r '.email')"
|
||||||
is_admin="$(printf '%s\n' "$user_spec" | jq -r '.isAdmin // false')"
|
is_admin="$(printf '%s\n' "$user_spec" | jq -r '.isAdmin // false')"
|
||||||
groups_json="$(printf '%s\n' "$user_spec" | jq -c '.groups // []')"
|
groups_json="$(printf '%s\n' "$user_spec" | jq -c '.groups // []')"
|
||||||
|
password_file="$(printf '%s\n' "$user_spec" | jq -r '.passwordFile // empty')"
|
||||||
|
|
||||||
if [[ -z "$username" || "$username" == "null" || -z "$email" || "$email" == "null" ]]; then
|
if [[ -z "$username" || "$username" == "null" || -z "$email" || "$email" == "null" ]]; then
|
||||||
echo "error: each Burrow Authentik user requires username and email" >&2
|
echo "error: each Burrow Authentik user requires username and email" >&2
|
||||||
|
|
@ -178,6 +179,19 @@ ensure_user() {
|
||||||
echo "error: could not create Authentik user ${username}" >&2
|
echo "error: could not create Authentik user ${username}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$password_file" ]]; then
|
||||||
|
if [[ ! -s "$password_file" ]]; then
|
||||||
|
echo "error: password file for Authentik user ${username} is missing: ${password_file}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
api POST "/api/v3/core/users/${user_pk}/set_password/" "$(
|
||||||
|
jq -cn \
|
||||||
|
--arg password "$(tr -d '\r\n' < "$password_file")" \
|
||||||
|
'{password: $password}'
|
||||||
|
)" >/dev/null
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
lookup_application_pk() {
|
lookup_application_pk() {
|
||||||
|
|
|
||||||
294
Scripts/authentik-sync-tailnet-auth-flow.sh
Executable file
294
Scripts/authentik-sync-tailnet-auth-flow.sh
Executable file
|
|
@ -0,0 +1,294 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
authentik_url="${AUTHENTIK_URL:-https://auth.burrow.net}"
|
||||||
|
bootstrap_token="${AUTHENTIK_BOOTSTRAP_TOKEN:-}"
|
||||||
|
provider_slug="${AUTHENTIK_TAILNET_PROVIDER_SLUG:-ts}"
|
||||||
|
authentication_flow_name="${AUTHENTIK_TAILNET_AUTHENTICATION_FLOW_NAME:-Burrow Tailnet Authentication}"
|
||||||
|
authentication_flow_slug="${AUTHENTIK_TAILNET_AUTHENTICATION_FLOW_SLUG:-burrow-tailnet-authentication}"
|
||||||
|
identification_stage_name="${AUTHENTIK_TAILNET_IDENTIFICATION_STAGE_NAME:-burrow-tailnet-identification-stage}"
|
||||||
|
password_stage_name="${AUTHENTIK_TAILNET_PASSWORD_STAGE_NAME:-burrow-tailnet-password-stage}"
|
||||||
|
user_login_stage_name="${AUTHENTIK_TAILNET_USER_LOGIN_STAGE_NAME:-burrow-tailnet-user-login-stage}"
|
||||||
|
google_source_slug="${AUTHENTIK_TAILNET_GOOGLE_SOURCE_SLUG:-google}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: Scripts/authentik-sync-tailnet-auth-flow.sh
|
||||||
|
|
||||||
|
Required environment:
|
||||||
|
AUTHENTIK_BOOTSTRAP_TOKEN
|
||||||
|
|
||||||
|
Optional environment:
|
||||||
|
AUTHENTIK_URL
|
||||||
|
AUTHENTIK_TAILNET_PROVIDER_SLUG
|
||||||
|
AUTHENTIK_TAILNET_AUTHENTICATION_FLOW_NAME
|
||||||
|
AUTHENTIK_TAILNET_AUTHENTICATION_FLOW_SLUG
|
||||||
|
AUTHENTIK_TAILNET_IDENTIFICATION_STAGE_NAME
|
||||||
|
AUTHENTIK_TAILNET_PASSWORD_STAGE_NAME
|
||||||
|
AUTHENTIK_TAILNET_USER_LOGIN_STAGE_NAME
|
||||||
|
AUTHENTIK_TAILNET_GOOGLE_SOURCE_SLUG
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$bootstrap_token" ]]; then
|
||||||
|
echo "error: AUTHENTIK_BOOTSTRAP_TOKEN is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
api() {
|
||||||
|
local method="$1"
|
||||||
|
local path="$2"
|
||||||
|
local data="${3:-}"
|
||||||
|
|
||||||
|
if [[ -n "$data" ]]; then
|
||||||
|
curl -fsS \
|
||||||
|
-X "$method" \
|
||||||
|
-H "Authorization: Bearer ${bootstrap_token}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$data" \
|
||||||
|
"${authentik_url}${path}"
|
||||||
|
else
|
||||||
|
curl -fsS \
|
||||||
|
-X "$method" \
|
||||||
|
-H "Authorization: Bearer ${bootstrap_token}" \
|
||||||
|
"${authentik_url}${path}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_authentik() {
|
||||||
|
for _ in $(seq 1 90); do
|
||||||
|
if curl -fsS "${authentik_url}/-/health/ready/" >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "error: Authentik did not become ready at ${authentik_url}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup_stage_by_name() {
|
||||||
|
local path="$1"
|
||||||
|
local name="$2"
|
||||||
|
|
||||||
|
api GET "${path}?page_size=200" \
|
||||||
|
| jq -c --arg name "$name" '.results[]? | select(.name == $name)' \
|
||||||
|
| head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup_flow_pk() {
|
||||||
|
local slug="$1"
|
||||||
|
|
||||||
|
api GET "/api/v3/flows/instances/?slug=${slug}" \
|
||||||
|
| jq -r '.results[]? | select(.slug != null) | .pk // empty' \
|
||||||
|
| head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup_source_pk() {
|
||||||
|
local slug="$1"
|
||||||
|
|
||||||
|
api GET "/api/v3/sources/oauth/?page_size=200&slug=${slug}" \
|
||||||
|
| jq -r --arg slug "$slug" '.results[]? | select(.slug == $slug) | .pk // empty' \
|
||||||
|
| head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_password_stage() {
|
||||||
|
local existing payload stage_pk
|
||||||
|
|
||||||
|
existing="$(lookup_stage_by_name "/api/v3/stages/password/" "$password_stage_name")"
|
||||||
|
payload="$(
|
||||||
|
jq -cn \
|
||||||
|
--arg name "$password_stage_name" \
|
||||||
|
'{
|
||||||
|
name: $name,
|
||||||
|
backends: [
|
||||||
|
"authentik.core.auth.InbuiltBackend",
|
||||||
|
"authentik.core.auth.TokenBackend"
|
||||||
|
],
|
||||||
|
allow_show_password: false,
|
||||||
|
failed_attempts_before_cancel: 5
|
||||||
|
}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -n "$existing" ]]; then
|
||||||
|
stage_pk="$(printf '%s\n' "$existing" | jq -r '.pk')"
|
||||||
|
api PATCH "/api/v3/stages/password/${stage_pk}/" "$payload" >/dev/null
|
||||||
|
else
|
||||||
|
stage_pk="$(
|
||||||
|
api POST "/api/v3/stages/password/" "$payload" \
|
||||||
|
| jq -r '.pk // empty'
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$stage_pk"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_identification_stage() {
|
||||||
|
local password_stage_pk="$1"
|
||||||
|
local google_source_pk="$2"
|
||||||
|
local existing payload stage_pk sources_json
|
||||||
|
|
||||||
|
existing="$(lookup_stage_by_name "/api/v3/stages/identification/" "$identification_stage_name")"
|
||||||
|
if [[ -n "$google_source_pk" ]]; then
|
||||||
|
sources_json="$(jq -cn --arg source "$google_source_pk" '[$source]')"
|
||||||
|
else
|
||||||
|
sources_json='[]'
|
||||||
|
fi
|
||||||
|
|
||||||
|
payload="$(
|
||||||
|
jq -cn \
|
||||||
|
--arg name "$identification_stage_name" \
|
||||||
|
--arg password_stage "$password_stage_pk" \
|
||||||
|
--argjson sources "$sources_json" \
|
||||||
|
'{
|
||||||
|
name: $name,
|
||||||
|
user_fields: ["username", "email"],
|
||||||
|
password_stage: $password_stage,
|
||||||
|
case_insensitive_matching: true,
|
||||||
|
show_matched_user: true,
|
||||||
|
sources: $sources,
|
||||||
|
show_source_labels: true,
|
||||||
|
pretend_user_exists: false,
|
||||||
|
enable_remember_me: false
|
||||||
|
}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -n "$existing" ]]; then
|
||||||
|
stage_pk="$(printf '%s\n' "$existing" | jq -r '.pk')"
|
||||||
|
api PATCH "/api/v3/stages/identification/${stage_pk}/" "$payload" >/dev/null
|
||||||
|
else
|
||||||
|
stage_pk="$(
|
||||||
|
api POST "/api/v3/stages/identification/" "$payload" \
|
||||||
|
| jq -r '.pk // empty'
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$stage_pk"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_user_login_stage() {
|
||||||
|
local existing payload stage_pk
|
||||||
|
|
||||||
|
existing="$(lookup_stage_by_name "/api/v3/stages/user_login/" "$user_login_stage_name")"
|
||||||
|
payload="$(
|
||||||
|
jq -cn \
|
||||||
|
--arg name "$user_login_stage_name" \
|
||||||
|
'{
|
||||||
|
name: $name,
|
||||||
|
session_duration: "hours=12",
|
||||||
|
terminate_other_sessions: false,
|
||||||
|
remember_me_offset: "seconds=0",
|
||||||
|
network_binding: "no_binding",
|
||||||
|
geoip_binding: "no_binding"
|
||||||
|
}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -n "$existing" ]]; then
|
||||||
|
stage_pk="$(printf '%s\n' "$existing" | jq -r '.pk')"
|
||||||
|
api PATCH "/api/v3/stages/user_login/${stage_pk}/" "$payload" >/dev/null
|
||||||
|
else
|
||||||
|
stage_pk="$(
|
||||||
|
api POST "/api/v3/stages/user_login/" "$payload" \
|
||||||
|
| jq -r '.pk // empty'
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$stage_pk"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_authentication_flow() {
|
||||||
|
local existing_pk payload
|
||||||
|
|
||||||
|
existing_pk="$(lookup_flow_pk "$authentication_flow_slug")"
|
||||||
|
payload="$(
|
||||||
|
jq -cn \
|
||||||
|
--arg name "$authentication_flow_name" \
|
||||||
|
--arg slug "$authentication_flow_slug" \
|
||||||
|
'{
|
||||||
|
name: $name,
|
||||||
|
title: $name,
|
||||||
|
slug: $slug,
|
||||||
|
designation: "authentication",
|
||||||
|
policy_engine_mode: "any",
|
||||||
|
layout: "stacked"
|
||||||
|
}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -n "$existing_pk" ]]; then
|
||||||
|
api PATCH "/api/v3/flows/instances/${authentication_flow_slug}/" "$payload" >/dev/null
|
||||||
|
printf '%s\n' "$existing_pk"
|
||||||
|
else
|
||||||
|
api POST "/api/v3/flows/instances/" "$payload" \
|
||||||
|
| jq -r '.pk // empty'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_flow_binding() {
|
||||||
|
local flow_pk="$1"
|
||||||
|
local stage_pk="$2"
|
||||||
|
local order="$3"
|
||||||
|
local existing payload binding_pk
|
||||||
|
|
||||||
|
existing="$(
|
||||||
|
api GET "/api/v3/flows/bindings/?target=${flow_pk}&stage=${stage_pk}&page_size=200" \
|
||||||
|
| jq -c '.results[]?' \
|
||||||
|
| head -n1
|
||||||
|
)"
|
||||||
|
|
||||||
|
payload="$(
|
||||||
|
jq -cn \
|
||||||
|
--arg target "$flow_pk" \
|
||||||
|
--arg stage "$stage_pk" \
|
||||||
|
--argjson order "$order" \
|
||||||
|
'{
|
||||||
|
target: $target,
|
||||||
|
stage: $stage,
|
||||||
|
order: $order,
|
||||||
|
policy_engine_mode: "any"
|
||||||
|
}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -n "$existing" ]]; then
|
||||||
|
binding_pk="$(printf '%s\n' "$existing" | jq -r '.pk')"
|
||||||
|
api PATCH "/api/v3/flows/bindings/${binding_pk}/" "$payload" >/dev/null
|
||||||
|
else
|
||||||
|
api POST "/api/v3/flows/bindings/" "$payload" >/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_authentik
|
||||||
|
|
||||||
|
provider_pk="$(
|
||||||
|
api GET "/api/v3/providers/oauth2/?page_size=200" \
|
||||||
|
| jq -r --arg provider_slug "$provider_slug" '
|
||||||
|
.results[]?
|
||||||
|
| select(.assigned_application_slug == $provider_slug or .slug == $provider_slug)
|
||||||
|
| .pk // empty
|
||||||
|
' \
|
||||||
|
| head -n1
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -z "$provider_pk" ]]; then
|
||||||
|
echo "error: could not resolve Authentik Tailnet OAuth provider ${provider_slug}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
google_source_pk="$(lookup_source_pk "$google_source_slug" || true)"
|
||||||
|
password_stage_pk="$(ensure_password_stage)"
|
||||||
|
identification_stage_pk="$(ensure_identification_stage "$password_stage_pk" "$google_source_pk")"
|
||||||
|
user_login_stage_pk="$(ensure_user_login_stage)"
|
||||||
|
authentication_flow_pk="$(ensure_authentication_flow)"
|
||||||
|
|
||||||
|
ensure_flow_binding "$authentication_flow_pk" "$identification_stage_pk" 10
|
||||||
|
ensure_flow_binding "$authentication_flow_pk" "$user_login_stage_pk" 30
|
||||||
|
|
||||||
|
api PATCH "/api/v3/providers/oauth2/${provider_pk}/" "$(
|
||||||
|
jq -cn --arg flow "$authentication_flow_pk" '{authentication_flow: $flow}'
|
||||||
|
)" >/dev/null
|
||||||
|
|
||||||
|
echo "Synced Burrow Tailnet authentication flow for provider ${provider_slug}."
|
||||||
73
Scripts/run-ios-tailnet-ui-tests.sh
Executable file
73
Scripts/run-ios-tailnet-ui-tests.sh
Executable file
|
|
@ -0,0 +1,73 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
bundle_id="${BURROW_UI_TEST_APP_BUNDLE_ID:-com.hackclub.burrow}"
|
||||||
|
simulator_name="${BURROW_UI_TEST_SIMULATOR_NAME:-iPhone 17 Pro}"
|
||||||
|
simulator_os="${BURROW_UI_TEST_SIMULATOR_OS:-26.4}"
|
||||||
|
derived_data_path="${BURROW_UI_TEST_DERIVED_DATA_PATH:-/tmp/burrow-ui-tests-deriveddata}"
|
||||||
|
source_packages_path="${BURROW_UI_TEST_SOURCE_PACKAGES_PATH:-/tmp/burrow-ui-tests-sourcepackages}"
|
||||||
|
fallback_dir="${HOME}/Library/Application Support/${bundle_id}/SimulatorFallback"
|
||||||
|
socket_path="${fallback_dir}/burrow.sock"
|
||||||
|
daemon_log="${BURROW_UI_TEST_DAEMON_LOG:-/tmp/burrow-ui-test-daemon.log}"
|
||||||
|
ui_test_email="${BURROW_UI_TEST_EMAIL:-ui-test@burrow.net}"
|
||||||
|
ui_test_username="${BURROW_UI_TEST_USERNAME:-ui-test}"
|
||||||
|
password_secret="${repo_root}/secrets/infra/authentik-ui-test-password.age"
|
||||||
|
age_identity="${BURROW_UI_TEST_AGE_IDENTITY:-${HOME}/.ssh/id_ed25519}"
|
||||||
|
|
||||||
|
ui_test_password="${BURROW_UI_TEST_PASSWORD:-}"
|
||||||
|
if [[ -z "$ui_test_password" ]]; then
|
||||||
|
if [[ -f "$password_secret" && -f "$age_identity" ]]; then
|
||||||
|
ui_test_password="$(age -d -i "$age_identity" "$password_secret" | tr -d '\r\n')"
|
||||||
|
else
|
||||||
|
echo "error: BURROW_UI_TEST_PASSWORD is unset and ${password_secret} could not be decrypted" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$fallback_dir" "$derived_data_path" "$source_packages_path"
|
||||||
|
rm -f "$socket_path"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n "${daemon_pid:-}" ]]; then
|
||||||
|
kill "$daemon_pid" >/dev/null 2>&1 || true
|
||||||
|
wait "$daemon_pid" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
cargo build -p burrow --bin burrow
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$fallback_dir"
|
||||||
|
BURROW_SOCKET_PATH="burrow.sock" \
|
||||||
|
"${repo_root}/target/debug/burrow" daemon >"$daemon_log" 2>&1
|
||||||
|
) &
|
||||||
|
daemon_pid=$!
|
||||||
|
|
||||||
|
for _ in $(seq 1 50); do
|
||||||
|
[[ -S "$socket_path" ]] && break
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -S "$socket_path" ]]; then
|
||||||
|
echo "error: Burrow daemon did not create ${socket_path}" >&2
|
||||||
|
[[ -f "$daemon_log" ]] && cat "$daemon_log" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BURROW_UI_TEST_EMAIL="$ui_test_email" \
|
||||||
|
BURROW_UI_TEST_USERNAME="$ui_test_username" \
|
||||||
|
BURROW_UI_TEST_PASSWORD="$ui_test_password" \
|
||||||
|
xcodebuild \
|
||||||
|
-quiet \
|
||||||
|
-skipPackagePluginValidation \
|
||||||
|
-project "${repo_root}/Apple/Burrow.xcodeproj" \
|
||||||
|
-scheme App \
|
||||||
|
-configuration Debug \
|
||||||
|
-destination "platform=iOS Simulator,name=${simulator_name},OS=${simulator_os}" \
|
||||||
|
-derivedDataPath "$derived_data_path" \
|
||||||
|
-clonedSourcePackagesDirPath "$source_packages_path" \
|
||||||
|
-only-testing:BurrowUITests \
|
||||||
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
|
test
|
||||||
|
|
@ -43,5 +43,18 @@
|
||||||
"automation"
|
"automation"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ui-test = {
|
||||||
|
displayName = "Burrow UI Test";
|
||||||
|
canonicalEmail = "ui-test@burrow.net";
|
||||||
|
isAdmin = false;
|
||||||
|
forgeAuthorized = false;
|
||||||
|
bootstrapAuthentik = true;
|
||||||
|
authentikPasswordSecret = "burrowAuthentikUiTestPassword";
|
||||||
|
roles = [
|
||||||
|
"testing"
|
||||||
|
"apple-ui"
|
||||||
|
];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
let
|
let
|
||||||
contributors = import ../../../contributors.nix;
|
contributors = import ../../../contributors.nix;
|
||||||
identities = contributors.identities;
|
identities = contributors.identities;
|
||||||
|
authentikPasswordSecretPath = identity:
|
||||||
|
if identity ? authentikPasswordSecret
|
||||||
|
then config.age.secrets.${identity.authentikPasswordSecret}.path
|
||||||
|
else null;
|
||||||
bootstrapUsers = lib.mapAttrsToList
|
bootstrapUsers = lib.mapAttrsToList
|
||||||
(
|
(
|
||||||
username: identity: {
|
username: identity: {
|
||||||
|
|
@ -11,6 +15,7 @@ let
|
||||||
email = identity.canonicalEmail;
|
email = identity.canonicalEmail;
|
||||||
sourceEmail = identity.sourceEmail or null;
|
sourceEmail = identity.sourceEmail or null;
|
||||||
isAdmin = identity.isAdmin or false;
|
isAdmin = identity.isAdmin or false;
|
||||||
|
passwordFile = authentikPasswordSecretPath identity;
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
(lib.filterAttrs (_: identity: identity.bootstrapAuthentik or false) identities);
|
(lib.filterAttrs (_: identity: identity.bootstrapAuthentik or false) identities);
|
||||||
|
|
@ -70,6 +75,12 @@ in
|
||||||
group = "root";
|
group = "root";
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
};
|
};
|
||||||
|
age.secrets.burrowAuthentikUiTestPassword = {
|
||||||
|
file = ../../../secrets/infra/authentik-ui-test-password.age;
|
||||||
|
owner = "root";
|
||||||
|
group = "root";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
|
|
||||||
networking.extraHosts = ''
|
networking.extraHosts = ''
|
||||||
127.0.0.1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net nsc-autoscaler.burrow.net
|
127.0.0.1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net nsc-autoscaler.burrow.net
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ let
|
||||||
directorySyncScript = ../../Scripts/authentik-sync-burrow-directory.sh;
|
directorySyncScript = ../../Scripts/authentik-sync-burrow-directory.sh;
|
||||||
forgejoOidcSyncScript = ../../Scripts/authentik-sync-forgejo-oidc.sh;
|
forgejoOidcSyncScript = ../../Scripts/authentik-sync-forgejo-oidc.sh;
|
||||||
googleSourceSyncScript = ../../Scripts/authentik-sync-google-source.sh;
|
googleSourceSyncScript = ../../Scripts/authentik-sync-google-source.sh;
|
||||||
|
tailnetAuthFlowSyncScript = ../../Scripts/authentik-sync-tailnet-auth-flow.sh;
|
||||||
authentikBlueprint = pkgs.writeText "burrow-authentik-blueprint.yaml" ''
|
authentikBlueprint = pkgs.writeText "burrow-authentik-blueprint.yaml" ''
|
||||||
version: 1
|
version: 1
|
||||||
metadata:
|
metadata:
|
||||||
|
|
@ -175,6 +176,36 @@ in
|
||||||
description = "Identification-stage behavior for the Google Authentik source.";
|
description = "Identification-stage behavior for the Google Authentik source.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
headscaleAuthenticationFlowSlug = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "burrow-tailnet-authentication";
|
||||||
|
description = "Authentik authentication flow slug used for Burrow Tailnet sign-in.";
|
||||||
|
};
|
||||||
|
|
||||||
|
headscaleAuthenticationFlowName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "Burrow Tailnet Authentication";
|
||||||
|
description = "Authentik authentication flow name used for Burrow Tailnet sign-in.";
|
||||||
|
};
|
||||||
|
|
||||||
|
headscaleIdentificationStageName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "burrow-tailnet-identification-stage";
|
||||||
|
description = "Authentik identification stage used for Burrow Tailnet sign-in.";
|
||||||
|
};
|
||||||
|
|
||||||
|
headscalePasswordStageName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "burrow-tailnet-password-stage";
|
||||||
|
description = "Authentik password stage used for Burrow Tailnet sign-in.";
|
||||||
|
};
|
||||||
|
|
||||||
|
headscaleUserLoginStageName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "burrow-tailnet-user-login-stage";
|
||||||
|
description = "Authentik user-login stage used for Burrow Tailnet sign-in.";
|
||||||
|
};
|
||||||
|
|
||||||
userGroupName = lib.mkOption {
|
userGroupName = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "burrow-users";
|
default = "burrow-users";
|
||||||
|
|
@ -217,6 +248,11 @@ in
|
||||||
default = false;
|
default = false;
|
||||||
description = "Whether this user should be in the Burrow admin group.";
|
description = "Whether this user should be in the Burrow admin group.";
|
||||||
};
|
};
|
||||||
|
passwordFile = lib.mkOption {
|
||||||
|
type = nullOr str;
|
||||||
|
default = null;
|
||||||
|
description = "Optional host-local file containing a bootstrap password for this user.";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
default = [ ];
|
default = [ ];
|
||||||
|
|
@ -468,7 +504,7 @@ EOF
|
||||||
restartTriggers = [
|
restartTriggers = [
|
||||||
directorySyncScript
|
directorySyncScript
|
||||||
cfg.envFile
|
cfg.envFile
|
||||||
];
|
] ++ lib.concatMap (user: lib.optional (user.passwordFile != null) user.passwordFile) cfg.bootstrapUsers;
|
||||||
path = [
|
path = [
|
||||||
pkgs.bash
|
pkgs.bash
|
||||||
pkgs.coreutils
|
pkgs.coreutils
|
||||||
|
|
@ -491,7 +527,7 @@ EOF
|
||||||
export AUTHENTIK_BURROW_ADMINS_GROUP=${lib.escapeShellArg cfg.adminGroupName}
|
export AUTHENTIK_BURROW_ADMINS_GROUP=${lib.escapeShellArg cfg.adminGroupName}
|
||||||
export AUTHENTIK_FORGEJO_APPLICATION_SLUG=${lib.escapeShellArg cfg.forgejoProviderSlug}
|
export AUTHENTIK_FORGEJO_APPLICATION_SLUG=${lib.escapeShellArg cfg.forgejoProviderSlug}
|
||||||
export AUTHENTIK_BURROW_DIRECTORY_JSON='${builtins.toJSON (map (user: {
|
export AUTHENTIK_BURROW_DIRECTORY_JSON='${builtins.toJSON (map (user: {
|
||||||
inherit (user) username name email isAdmin;
|
inherit (user) username name email isAdmin passwordFile;
|
||||||
groups = user.groups;
|
groups = user.groups;
|
||||||
}) cfg.bootstrapUsers)}'
|
}) cfg.bootstrapUsers)}'
|
||||||
|
|
||||||
|
|
@ -499,6 +535,59 @@ EOF
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
systemd.services.burrow-authentik-tailnet-auth-flow = {
|
||||||
|
description = "Reconcile the Burrow Tailnet authentication flow";
|
||||||
|
after =
|
||||||
|
[
|
||||||
|
"burrow-authentik-ready.service"
|
||||||
|
"network-online.target"
|
||||||
|
]
|
||||||
|
++ lib.optionals (
|
||||||
|
cfg.googleClientIDFile != null && cfg.googleClientSecretFile != null
|
||||||
|
) [ "burrow-authentik-google-source.service" ];
|
||||||
|
wants =
|
||||||
|
[
|
||||||
|
"burrow-authentik-ready.service"
|
||||||
|
"network-online.target"
|
||||||
|
]
|
||||||
|
++ lib.optionals (
|
||||||
|
cfg.googleClientIDFile != null && cfg.googleClientSecretFile != null
|
||||||
|
) [ "burrow-authentik-google-source.service" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
restartTriggers = [
|
||||||
|
tailnetAuthFlowSyncScript
|
||||||
|
cfg.envFile
|
||||||
|
];
|
||||||
|
path = [
|
||||||
|
pkgs.bash
|
||||||
|
pkgs.coreutils
|
||||||
|
pkgs.curl
|
||||||
|
pkgs.jq
|
||||||
|
];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
User = "root";
|
||||||
|
Group = "root";
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
set -euo pipefail
|
||||||
|
set -a
|
||||||
|
source ${lib.escapeShellArg cfg.envFile}
|
||||||
|
set +a
|
||||||
|
|
||||||
|
export AUTHENTIK_URL=https://${cfg.domain}
|
||||||
|
export AUTHENTIK_TAILNET_PROVIDER_SLUG=${lib.escapeShellArg cfg.headscaleProviderSlug}
|
||||||
|
export AUTHENTIK_TAILNET_AUTHENTICATION_FLOW_NAME=${lib.escapeShellArg cfg.headscaleAuthenticationFlowName}
|
||||||
|
export AUTHENTIK_TAILNET_AUTHENTICATION_FLOW_SLUG=${lib.escapeShellArg cfg.headscaleAuthenticationFlowSlug}
|
||||||
|
export AUTHENTIK_TAILNET_IDENTIFICATION_STAGE_NAME=${lib.escapeShellArg cfg.headscaleIdentificationStageName}
|
||||||
|
export AUTHENTIK_TAILNET_PASSWORD_STAGE_NAME=${lib.escapeShellArg cfg.headscalePasswordStageName}
|
||||||
|
export AUTHENTIK_TAILNET_USER_LOGIN_STAGE_NAME=${lib.escapeShellArg cfg.headscaleUserLoginStageName}
|
||||||
|
export AUTHENTIK_TAILNET_GOOGLE_SOURCE_SLUG=${lib.escapeShellArg cfg.googleSourceSlug}
|
||||||
|
|
||||||
|
${pkgs.bash}/bin/bash ${tailnetAuthFlowSyncScript}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
systemd.services.burrow-authentik-forgejo-oidc = lib.mkIf (cfg.forgejoClientSecretFile != null) {
|
systemd.services.burrow-authentik-forgejo-oidc = lib.mkIf (cfg.forgejoClientSecretFile != null) {
|
||||||
description = "Reconcile the Burrow Authentik Forgejo OIDC application";
|
description = "Reconcile the Burrow Authentik Forgejo OIDC application";
|
||||||
after = [
|
after = [
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ in
|
||||||
"secrets/infra/authentik.env.age".publicKeys = burrowForgeRecipients;
|
"secrets/infra/authentik.env.age".publicKeys = burrowForgeRecipients;
|
||||||
"secrets/infra/authentik-google-client-id.age".publicKeys = burrowForgeRecipients;
|
"secrets/infra/authentik-google-client-id.age".publicKeys = burrowForgeRecipients;
|
||||||
"secrets/infra/authentik-google-client-secret.age".publicKeys = burrowForgeRecipients;
|
"secrets/infra/authentik-google-client-secret.age".publicKeys = burrowForgeRecipients;
|
||||||
|
"secrets/infra/authentik-ui-test-password.age".publicKeys = burrowForgeRecipients;
|
||||||
"secrets/infra/forgejo-oidc-client-secret.age".publicKeys = burrowForgeRecipients;
|
"secrets/infra/forgejo-oidc-client-secret.age".publicKeys = burrowForgeRecipients;
|
||||||
"secrets/infra/headscale-oidc-client-secret.age".publicKeys = burrowForgeRecipients;
|
"secrets/infra/headscale-oidc-client-secret.age".publicKeys = burrowForgeRecipients;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
secrets/infra/authentik-ui-test-password.age
Normal file
9
secrets/infra/authentik-ui-test-password.age
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
age-encryption.org/v1
|
||||||
|
-> ssh-ed25519 ux4N8Q 4+zOIEyQTCHqKdZKV/H4D7e4y+UTrc9rYzvCgGUPVEg
|
||||||
|
S+tAlc4wvzVUe9r9+mBAnUj5C31bQqo4PK3muBCzs2Y
|
||||||
|
-> ssh-ed25519 IrZmAg 1KasjHiY1MQVLIzoDdGshhDhaDimOtZ5EyE4GyZngHg
|
||||||
|
ov711Sp+Q/zQw0NUpB2rnKEF8bFxoVafdVQ/8gSbSZA
|
||||||
|
-> X25519 3EWdCP5UkWd1g6bDaQm/kNCNlhSONrz8RB7OZgT9nXE
|
||||||
|
6+HoM9mg6P/CtU39P8SCyutLkmYw27MikoZZ5L9nI54
|
||||||
|
--- Rw0o+MvtvHQrrYPNtCPxHGR67K67nyJUQRd4DN3nOCY
|
||||||
|
<EFBFBD>fëŸnÜ<eOt€éªYÑR|óèP8÷sE=^”Y-[T€Qj8]€\Èÿ¬‚4¢™õ«
|
||||||
Loading…
Add table
Add a link
Reference in a new issue