Skip to content

Commit 87ce4ff

Browse files
committed
Merge branch 'feature/saml-login-everywhere'
2 parents d0a43f8 + 4ce0f12 commit 87ce4ff

78 files changed

Lines changed: 4893 additions & 3234 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

palace-accessibility/src/main/java/org/nypl/simplified/accessibility/AccessibilityService.kt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,28 @@ class AccessibilityService private constructor(
114114

115115
is BookStatus.ReachedLoanLimit ->
116116
if (this.previousStatusIsNot(event, BookStatus.ReachedLoanLimit::class.java)) {
117-
this.speak(this.strings.bookLoanLimitReached())
117+
this.speak(this.strings.bookLoanLimitReached(book.entry.title))
118+
} else {
119+
// Nothing to do
120+
}
121+
122+
is BookStatus.FailedLoanBadCredentials ->
123+
if (this.previousStatusIsNot(event, BookStatus.FailedLoanBadCredentials::class.java)) {
124+
this.speak(this.strings.bookFailedLoanLoginRequired(book.entry.title))
125+
} else {
126+
// Nothing to do
127+
}
128+
129+
is BookStatus.FailedRevokeBadCredentials ->
130+
if (this.previousStatusIsNot(event, BookStatus.FailedRevokeBadCredentials::class.java)) {
131+
this.speak(this.strings.bookFailedRevokeLoginRequired(book.entry.title))
132+
} else {
133+
// Nothing to do
134+
}
135+
136+
is BookStatus.FailedDownloadBadCredentials ->
137+
if (this.previousStatusIsNot(event, BookStatus.FailedDownloadBadCredentials::class.java)) {
138+
this.speak(this.strings.bookFailedDownloadLoginRequired(book.entry.title))
118139
} else {
119140
// Nothing to do
120141
}

palace-accessibility/src/main/java/org/nypl/simplified/accessibility/AccessibilityStrings.kt

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,35 @@ class AccessibilityStrings(
1111
private val resources: Resources
1212
) : AccessibilityStringsType {
1313
override fun bookHasDownloaded(title: String): String =
14-
this.resources.getString(R.string.bookHasDownloaded, title)
14+
this.resources.getString(R.string.accessBookHasDownloaded, title)
1515

1616
override fun bookIsDownloading(title: String): String =
17-
this.resources.getString(R.string.bookIsDownloading, title)
17+
this.resources.getString(R.string.accessBookIsDownloading, title)
1818

1919
override fun bookIsOnHold(title: String): String =
20-
this.resources.getString(R.string.bookIsOnHold, title)
20+
this.resources.getString(R.string.accessBookIsOnHold, title)
2121

2222
override fun bookReturned(title: String): String =
23-
this.resources.getString(R.string.bookReturned, title)
23+
this.resources.getString(R.string.accessBookReturned, title)
2424

2525
override fun bookFailedReturn(title: String): String =
26-
this.resources.getString(R.string.bookFailedReturn, title)
26+
this.resources.getString(R.string.accessBookFailedReturn, title)
2727

2828
override fun bookFailedLoan(title: String): String =
29-
this.resources.getString(R.string.bookFailedLoan, title)
29+
this.resources.getString(R.string.accessBookFailedLoan, title)
3030

3131
override fun bookFailedDownload(title: String): String =
32-
this.resources.getString(R.string.bookFailedDownload, title)
32+
this.resources.getString(R.string.accessBookFailedDownload, title)
3333

34-
override fun bookLoanLimitReached(): String =
35-
this.resources.getString(R.string.reachedLoanLimit)
34+
override fun bookLoanLimitReached(title: String): String =
35+
this.resources.getString(R.string.accessBookReachedLoanLimit, title)
36+
37+
override fun bookFailedLoanLoginRequired(title: String): String =
38+
this.resources.getString(R.string.accessBookFailedLoanLoginRequired, title)
39+
40+
override fun bookFailedRevokeLoginRequired(title: String): String =
41+
this.resources.getString(R.string.accessBookFailedRevokeLoginRequired, title)
42+
43+
override fun bookFailedDownloadLoginRequired(title: String): String =
44+
this.resources.getString(R.string.accessBookFailedDownloadLoginRequired, title)
3645
}

palace-accessibility/src/main/java/org/nypl/simplified/accessibility/AccessibilityStringsType.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@ interface AccessibilityStringsType {
1212
fun bookFailedReturn(title: String): String
1313
fun bookFailedLoan(title: String): String
1414
fun bookFailedDownload(title: String): String
15-
fun bookLoanLimitReached(): String
15+
fun bookLoanLimitReached(title: String): String
16+
fun bookFailedLoanLoginRequired(title: String): String
17+
fun bookFailedRevokeLoginRequired(title: String): String
18+
fun bookFailedDownloadLoginRequired(title: String): String
1619
}
Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
<?xml version="1.0" encoding="utf-8"?>
22

33
<resources>
4-
<string name="bookFailedDownload">@string/bookFailedLoan</string>
5-
<string name="bookFailedLoan">The book \'%1$s\' could not be downloaded</string>
6-
<string name="bookFailedReturn">The book \'%1$s\' could not be returned</string>
7-
<string name="bookHasDownloaded">The book \'%1$s\' has finished downloading</string>
8-
<string name="bookIsDownloading">The book \'%1$s\' has started downloading</string>
9-
<string name="bookIsOnHold">A reservation has been placed for the book \'%1$s\'</string>
10-
<string name="bookReturned">The book \'%1$s\' has been successfully returned</string>
11-
<string name="reachedLoanLimit">You have reached your loan limit</string>
4+
<string name="accessBookFailedDownload">@string/accessBookFailedLoan</string>
5+
<string name="accessBookFailedLoan">The book \'%1$s\' could not be downloaded</string>
6+
<string name="accessBookFailedReturn">The book \'%1$s\' could not be returned</string>
7+
<string name="accessBookHasDownloaded">The book \'%1$s\' has finished downloading</string>
8+
<string name="accessBookIsDownloading">The book \'%1$s\' has started downloading</string>
9+
<string name="accessBookIsOnHold">A reservation has been placed for the book \'%1$s\'</string>
10+
<string name="accessBookReturned">The book \'%1$s\' has been successfully returned</string>
11+
<string name="accessBookReachedLoanLimit">The book \'%1$s\' could not be borrowed because you have reached your loan limit</string>
12+
<string name="accessBookFailedLoanLoginRequired">The book \'%1$s\' could not be downloaded because your login session has expired.</string>
13+
<string name="accessBookFailedRevokeLoginRequired">The book \'%1$s\' could not be returned because your login session has expired.</string>
14+
<string name="accessBookFailedDownloadLoginRequired">@string/accessBookFailedLoanLoginRequired</string>
1215
</resources>

palace-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountAuthenticatedHTTP.kt

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package org.nypl.simplified.accounts.api
33
import org.librarysimplified.http.api.LSHTTPAuthorizationBasic
44
import org.librarysimplified.http.api.LSHTTPAuthorizationBearerToken
55
import org.librarysimplified.http.api.LSHTTPAuthorizationType
6+
import org.librarysimplified.http.api.LSHTTPProblemReport
67
import org.librarysimplified.http.api.LSHTTPRequestBuilderType
78
import org.librarysimplified.http.api.LSHTTPRequestConstants
89
import org.librarysimplified.http.api.LSHTTPResponseStatus
10+
import org.nypl.simplified.accounts.api.AccountAuthenticatedHTTP.Handled401.ErrorIsRecoverableCredentialsExpired
11+
import org.nypl.simplified.accounts.api.AccountAuthenticatedHTTP.Handled401.ErrorIsUnrecoverable
912

1013
/**
1114
* Convenient functions to construct authenticated HTTP instances from sets of credentials.
@@ -22,6 +25,7 @@ object AccountAuthenticatedHTTP {
2225
userName = credentials.userName.value,
2326
password = credentials.password.value
2427
)
28+
2529
is AccountAuthenticationCredentials.BasicToken ->
2630
if (credentials.authenticationTokenInfo.accessToken.isNotBlank()) {
2731
LSHTTPAuthorizationBearerToken.ofToken(
@@ -33,10 +37,12 @@ object AccountAuthenticatedHTTP {
3337
password = credentials.password.value
3438
)
3539
}
40+
3641
is AccountAuthenticationCredentials.OAuthWithIntermediary ->
3742
LSHTTPAuthorizationBearerToken.ofToken(
3843
token = credentials.accessToken
3944
)
45+
4046
is AccountAuthenticationCredentials.SAML2_0 ->
4147
LSHTTPAuthorizationBearerToken.ofToken(
4248
token = credentials.accessToken
@@ -50,13 +56,18 @@ object AccountAuthenticatedHTTP {
5056
return credentials?.let(this::createAuthorization)
5157
}
5258

53-
fun LSHTTPRequestBuilderType.addCredentialsToProperties(
59+
fun LSHTTPRequestBuilderType.addBasicTokenPropertiesIfApplicable(
5460
credentials: AccountAuthenticationCredentials?
5561
): LSHTTPRequestBuilderType {
5662
if (credentials !is AccountAuthenticationCredentials.BasicToken) {
5763
return this
5864
}
65+
return addBasicTokenProperties(credentials)
66+
}
5967

68+
fun LSHTTPRequestBuilderType.addBasicTokenProperties(
69+
credentials: AccountAuthenticationCredentials.BasicToken
70+
): LSHTTPRequestBuilderType {
6071
return apply {
6172
setExtensionProperty(
6273
LSHTTPRequestConstants.PROPERTY_KEY_USERNAME,
@@ -76,4 +87,47 @@ object AccountAuthenticatedHTTP {
7687
fun LSHTTPResponseStatus.getAccessToken(): String? {
7788
return properties?.header(LSHTTPRequestConstants.PROPERTY_KEY_ACCESS_TOKEN)
7889
}
90+
91+
/**
92+
* A handled 401 status code.
93+
*/
94+
95+
sealed class Handled401 {
96+
/** A recoverable error; ask the user to log in again. */
97+
data object ErrorIsUnrecoverable : Handled401()
98+
99+
/** An unrecoverable error; just fail. */
100+
data object ErrorIsRecoverableCredentialsExpired : Handled401()
101+
}
102+
103+
/**
104+
* Handle a 401 error along with a possibly-not-present problem report.
105+
*
106+
* The server will return useful problem reports that indicate whether a 401 error is
107+
* recoverable or not. If an error _is_ recoverable, then the app should ask the user to log
108+
* in again. If an error isn't recoverable, then it shouldn't be treated any differently to
109+
* any other kind of error.
110+
*/
111+
112+
fun handle401Error(
113+
problemReport: LSHTTPProblemReport?
114+
): Handled401 {
115+
return if (problemReport != null) {
116+
val type = problemReport.type
117+
if (type != null) {
118+
// XXX: Deprecated: The server will soon stop serving this type.
119+
if (type.startsWith("http://librarysimplified.org/terms/problem/")) {
120+
ErrorIsRecoverableCredentialsExpired
121+
} else if (type.startsWith("http://palaceproject.io/terms/problem/auth/recoverable")) {
122+
ErrorIsRecoverableCredentialsExpired
123+
} else {
124+
ErrorIsUnrecoverable
125+
}
126+
} else {
127+
ErrorIsUnrecoverable
128+
}
129+
} else {
130+
ErrorIsUnrecoverable
131+
}
132+
}
79133
}

palace-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountLoginState.kt

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,27 @@ import org.nypl.simplified.taskrecorder.api.TaskResult
99

1010
sealed class AccountLoginState {
1111

12+
/**
13+
* The previous credentials, if credentials are being refreshed.
14+
*/
15+
16+
abstract val previousCredentials: AccountAuthenticationCredentials?
17+
18+
/**
19+
* The current assumed-valid credentials.
20+
*/
21+
1222
abstract val credentials: AccountAuthenticationCredentials?
1323

1424
/**
1525
* The account is not logged in.
1626
*/
1727

18-
object AccountNotLoggedIn : AccountLoginState() {
28+
data class AccountNotLoggedIn(
29+
override val previousCredentials: AccountAuthenticationCredentials?
30+
) : AccountLoginState() {
1931
override val credentials: AccountAuthenticationCredentials?
20-
get() = null
32+
get() = this.previousCredentials
2133

2234
override fun toString(): String =
2335
this.javaClass.simpleName
@@ -28,6 +40,7 @@ sealed class AccountLoginState {
2840
*/
2941

3042
data class AccountLoggingIn(
43+
override val previousCredentials: AccountAuthenticationCredentials?,
3144

3245
/**
3346
* A humanly-readable status message.
@@ -50,14 +63,19 @@ sealed class AccountLoginState {
5063
val cancellable: Boolean
5164
) : AccountLoginState() {
5265
override val credentials: AccountAuthenticationCredentials?
53-
get() = null
66+
get() = this.previousCredentials
5467
}
5568

5669
/**
5770
* The account is currently waiting for an external authentication mechanism to complete.
5871
*/
5972

6073
data class AccountLoggingInWaitingForExternalAuthentication(
74+
/**
75+
* The previous credentials, if credentials are being refreshed.
76+
*/
77+
78+
override val previousCredentials: AccountAuthenticationCredentials?,
6179

6280
/**
6381
* The description being used to log in.
@@ -74,14 +92,19 @@ sealed class AccountLoginState {
7492
val status: String
7593
) : AccountLoginState() {
7694
override val credentials: AccountAuthenticationCredentials?
77-
get() = null
95+
get() = this.previousCredentials
7896
}
7997

8098
/**
8199
* The account failed to log in.
82100
*/
83101

84102
data class AccountLoginFailed(
103+
/**
104+
* The previous credentials, if credentials are being refreshed.
105+
*/
106+
107+
override val previousCredentials: AccountAuthenticationCredentials?,
85108
val taskResult: TaskResult.Failure<*>
86109
) : AccountLoginState(), PresentableErrorType {
87110
override val message: String =
@@ -92,7 +115,7 @@ sealed class AccountLoginState {
92115
this.taskResult.attributes
93116

94117
override val credentials: AccountAuthenticationCredentials?
95-
get() = null
118+
get() = this.previousCredentials
96119
}
97120

98121
/**
@@ -101,7 +124,22 @@ sealed class AccountLoginState {
101124

102125
data class AccountLoggedIn(
103126
override val credentials: AccountAuthenticationCredentials
104-
) : AccountLoginState()
127+
) : AccountLoginState() {
128+
override val previousCredentials: AccountAuthenticationCredentials =
129+
this.credentials
130+
}
131+
132+
/**
133+
* The account is currently logged in but the credentials appear to be stale. This can be due
134+
* to, for example, expired SAML or OIDC sessions.
135+
*/
136+
137+
data class AccountLoggedInStaleCredentials(
138+
override val credentials: AccountAuthenticationCredentials
139+
) : AccountLoginState() {
140+
override val previousCredentials: AccountAuthenticationCredentials =
141+
this.credentials
142+
}
105143

106144
/**
107145
* The account is currently logging out.
@@ -117,7 +155,10 @@ sealed class AccountLoginState {
117155
*/
118156

119157
val status: String
120-
) : AccountLoginState()
158+
) : AccountLoginState() {
159+
override val previousCredentials: AccountAuthenticationCredentials =
160+
this.credentials
161+
}
121162

122163
/**
123164
* The account failed to log out
@@ -127,6 +168,8 @@ sealed class AccountLoginState {
127168
val taskResult: TaskResult.Failure<*>,
128169
override val credentials: AccountAuthenticationCredentials
129170
) : AccountLoginState(), PresentableErrorType {
171+
override val previousCredentials: AccountAuthenticationCredentials =
172+
this.credentials
130173
override val message: String =
131174
this.taskResult.message
132175
override val exception: Throwable? =

palace-accounts-database-api/src/main/java/org/nypl/simplified/accounts/database/api/AccountType.kt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ import org.nypl.simplified.books.book_database.api.BookDatabaseType
1111
* The interface exposed by accounts.
1212
*
1313
* An account aggregates a set of credentials and a book database.
14-
* Account are assigned monotonically increasing identifiers by the
15-
* application, but the identifiers themselves carry no meaning. It is
16-
* an error to depend on the values of identifiers for any kind of
17-
* program logic.
1814
*/
1915

2016
interface AccountType : AccountReadableType {
@@ -73,14 +69,16 @@ interface AccountType : AccountReadableType {
7369
return when (val state = this.loginState) {
7470
is AccountLoginState.AccountLoggedIn ->
7571
this.setLoginState(state.copy(update.invoke(state.credentials)))
72+
is AccountLoginState.AccountLoggedInStaleCredentials ->
73+
this.setLoginState(state.copy(update.invoke(state.credentials)))
7674
is AccountLoginState.AccountLoggingOut ->
7775
this.setLoginState(state.copy(update.invoke(state.credentials)))
7876

7977
is AccountLoginState.AccountLoggingIn,
8078
is AccountLoginState.AccountLoggingInWaitingForExternalAuthentication,
8179
is AccountLoginState.AccountLoginFailed,
8280
is AccountLoginState.AccountLogoutFailed,
83-
AccountLoginState.AccountNotLoggedIn ->
81+
is AccountLoginState.AccountNotLoggedIn ->
8482
Unit
8583
}
8684
}
@@ -112,4 +110,11 @@ interface AccountType : AccountReadableType {
112110
fun isBookmarkSyncable(): Boolean {
113111
return this.loginState.credentials?.annotationsURI != null
114112
}
113+
114+
/**
115+
* Mark the current credentials as having expired, assuming they are credentials of a type
116+
* that does expire.
117+
*/
118+
119+
fun expireCredentialsIfApplicable()
115120
}

0 commit comments

Comments
 (0)