Skip to content

Commit

Permalink
basic guest auth endpoint filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
mickmaccallum committed Aug 11, 2024
1 parent 8c00451 commit 8a6f956
Show file tree
Hide file tree
Showing 13 changed files with 103 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,16 @@ struct RoadmapVoteView: View {
)
),
onPress: {
contentObserver.currentTicketToVote = ticketContent.id
if parraAuthState.isGuest {
alertManager.showErrorToast(
userFacingMessage: "You need to sign in to vote on roadmap items.",
underlyingError: ParraError.guestNotPermitted(
action: "Vote on roadmap"
)
)
} else {
contentObserver.currentTicketToVote = ticketContent.id
}
}
)

Expand All @@ -70,5 +79,9 @@ struct RoadmapVoteView: View {
@EnvironmentObject private var contentObserver: RoadmapWidget
.ContentObserver
@EnvironmentObject private var componentFactory: ComponentFactory
@EnvironmentObject private var alertManager: AlertManager

@Environment(\.parra) private var parra
@Environment(\.parraTheme) private var parraTheme
@Environment(\.parraAuthState) private var parraAuthState
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ struct RoadmapWidget: Container {
.environment(config)
.environmentObject(contentObserver)
.environmentObject(componentFactory)
.environmentObject(alertManager)
.presentParraFeedbackForm(
with: $contentObserver.addRequestForm,
onDismiss: { dismissType in
Expand Down
8 changes: 1 addition & 7 deletions ios/Sources/Parra/Core/Internal/ParraInternal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,18 +186,12 @@ class ParraInternal {
) async {
logger.debug("Handle login flow")

var updatedUser = user

await sessionManager.initializeSessions()

do {
logger.debug("Refreshing user info")

// If refresh was successful, we need to make sure this is the
// user that becomes immediately accessible outside the SDK.
if let next = try await authService.refreshUserInfo() {
updatedUser = next
}
try await authService.refreshUserInfo()
} catch {
logger.error("Failed to refresh user info on app launch", error)
}
Expand Down
11 changes: 11 additions & 0 deletions ios/Sources/Parra/Core/Parra App/Auth/ParraAuthState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,15 @@ public enum ParraAuthState: Equatable, CustomStringConvertible {
return false
}
}

var credential: ParraUser.Credential? {
switch self {
case .authenticated(let user), .anonymous(let user):
return user.credential
case .guest(let guest):
return guest.credential
case .error, .undetermined:
return nil
}
}
}
2 changes: 1 addition & 1 deletion ios/Sources/Parra/Core/Parra App/ParraApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ public struct ParraApp<
@StateObject private var launchScreenState: LaunchScreenStateManager

@StateObject private var alertManager: AlertManager
@State private var authStateManager: ParraAuthStateManager = .default
@State private var authStateManager: ParraAuthStateManager = .shared
@State private var themeManager: ParraThemeManager = .shared

private let parra: ParraInternal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,5 @@ struct ParraCardViewPreview<Content>: View where Content: View {
private let configuration: ParraConfiguration
private let parra: Parra

@State private var authStateManager: ParraAuthStateManager = .default
@State private var authStateManager: ParraAuthStateManager = .shared
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ struct ParraContainerPreview<ContainerType>: View
theme: theme
)
self._authStateManager = State(
initialValue: ParraAuthStateManager.default
initialValue: ParraAuthStateManager.shared
)

let appState = ParraAppState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ struct ParraViewPreview<Content>: View where Content: View {
)

self._authStateManager = State(
initialValue: ParraAuthStateManager.default
initialValue: ParraAuthStateManager.shared
)

let appState = ParraAppState(
Expand Down
18 changes: 15 additions & 3 deletions ios/Sources/Parra/Networking/Servers/Auth/AuthService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -382,10 +382,10 @@ final class AuthService {
}

func applyUserUpdate(
_ authResult: ParraAuthState,
_ authState: ParraAuthState,
shouldBroadcast: Bool = true
) async {
switch authResult {
switch authState {
case .anonymous(let user):
logger.debug("Anonymous user authenticated")

Expand Down Expand Up @@ -423,6 +423,8 @@ final class AuthService {
_cachedCredential = nil
}

_cachedAuthState = authState

logger.trace("User update applied", [
"broadcasting": String(shouldBroadcast)
])
Expand All @@ -432,7 +434,7 @@ final class AuthService {
name: Parra.authenticationStateDidChangeNotification,
object: nil,
userInfo: [
Parra.authenticationStateKey: authResult
Parra.authenticationStateKey: authState
]
)
}
Expand Down Expand Up @@ -478,10 +480,15 @@ final class AuthService {
}
}

func getCachedAuthState() -> ParraAuthState? {
return _cachedAuthState
}

// MARK: - Private

// The actual cached token.
private var _cachedCredential: ParraUser.Credential?
private var _cachedAuthState: ParraAuthState?

private func performUnauthenticatedLogin(
appInfo: ParraAppInfo
Expand Down Expand Up @@ -555,6 +562,11 @@ final class AuthService {

if let user = await dataManager.getCurrentUser() {
_cachedCredential = user.credential
_cachedAuthState = if user.info.isAnonymous {
.anonymous(user)
} else {
.authenticated(user)
}

return _cachedCredential
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ final class ApiResourceServer: Server {
body: some Encodable,
timeout: TimeInterval? = nil
) async -> AuthenticatedRequestResult<T> {
guard await canHitEndpoint(endpoint) else {
return AuthenticatedRequestResult(
result: .failure(
ParraError.guestNotPermitted(
action: endpoint.displayName
)
),
responseContext: AuthenticatedRequestResponseContext(
attributes: [],
statusCode: nil
)
)
}

logger
.trace(
"Performing request to API endpoint: \(endpoint.slugWithMethod)"
Expand Down Expand Up @@ -198,6 +212,24 @@ final class ApiResourceServer: Server {

// MARK: - Private

private func canHitEndpoint(
_ endpoint: ApiEndpoint
) async -> Bool {
guard let authState = await authService.getCachedAuthState() else {
return false
}

// Temporary until more advanced scope work is done.
switch authState {
case .authenticated, .anonymous:
return true
case .guest:
return endpoint.allowsGuestAuth
case .error, .undetermined:
return false
}
}

// Perform request wrapper that automatically handles reauthentication and
// retrying if the request fails unauthenticated/unauthorized.
private func performReauthenticatingRequest(
Expand Down
2 changes: 1 addition & 1 deletion ios/Sources/Parra/State/ParraAuthStateManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class ParraAuthStateManager: CustomStringConvertible {

// MARK: - Internal

static let `default` = ParraAuthStateManager(
static let shared = ParraAuthStateManager(
current: .undetermined
)

Expand Down
13 changes: 13 additions & 0 deletions ios/Sources/Parra/Types/Network/Endpoints/ApiEndpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,17 @@ enum ApiEndpoint: Endpoint {
return false
}
}

var allowsGuestAuth: Bool {
// "scope": "guest parra:app:app_info.read parra:app:core:roadmap.read parra:app:core:tickets.read parra:app:core:releases.read parra:app:core:release.read parra:app:core:cards.read parra:app:core:feedback_form.read parra:app:core:feedback_form.submit"

switch self {
case .getAppInfo, .getRoadmap, .getPaginateTickets,
.getPaginateReleases, .getRelease, .getCards, .getFeedbackForm,
.postSubmitFeedbackForm:
return true
default:
return false
}
}
}
12 changes: 11 additions & 1 deletion ios/Sources/Parra/Types/ParraError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public enum ParraError: LocalizedError, CustomStringConvertible {
case validationFailed(failures: [String: String])
case unknown
case rateLimited
/// Thrown when an action is attempted by a user using guest auth, that
/// requires being authenticated anonymously or via login. The action
/// associated value is a string describing the action the user attempted
/// to take that wasn't permitted.
case guestNotPermitted(action: String)

// MARK: - Public

Expand Down Expand Up @@ -90,6 +95,8 @@ public enum ParraError: LocalizedError, CustomStringConvertible {
return failures.reduce(baseMessage) { partialResult, next in
return "\(partialResult)\n\(next.key): \(next.value)"
}
case .guestNotPermitted:
return "Not permitted to perform this action. Please login and try again."
}
}

Expand Down Expand Up @@ -154,6 +161,8 @@ public enum ParraError: LocalizedError, CustomStringConvertible {
return "Validation failed."
case .rateLimited:
return "Rate limited"
case .guestNotPermitted(let action):
return "Guests are not permitted to perform the action: \(action)"
}
}

Expand Down Expand Up @@ -216,7 +225,8 @@ extension ParraError: ParraSanitizedDictionaryConvertible {
"error_description": message
]
case .notInitialized, .message, .jsonError, .unknown, .system,
.rateLimited, .parraAuthenticationRequired, .unauthenticated:
.rateLimited, .parraAuthenticationRequired, .unauthenticated,
.guestNotPermitted:

return [:]
case .validationFailed(let failures):
Expand Down

0 comments on commit 8a6f956

Please sign in to comment.