@@ -6,6 +6,7 @@ package io.airbyte.commons.server.authorization
66
77import io.airbyte.api.problems.model.generated.ProblemMessageData
88import io.airbyte.api.problems.throwable.generated.ForbiddenProblem
9+ import io.airbyte.commons.DEFAULT_USER_ID
910import io.airbyte.commons.auth.AuthRole
1011import io.airbyte.commons.auth.AuthRoleConstants
1112import io.airbyte.commons.auth.OrganizationAuthRole
@@ -19,9 +20,14 @@ import io.airbyte.commons.server.support.CurrentUserService
1920import io.airbyte.config.Permission
2021import io.airbyte.config.Permission.PermissionType
2122import io.airbyte.config.helpers.PermissionHelper
23+ import io.airbyte.data.TokenType
24+ import io.airbyte.featureflag.FeatureFlagClient
25+ import io.airbyte.featureflag.IgnoreTokenRoleClaims
26+ import io.airbyte.featureflag.TokenSubject
2227import io.github.oshai.kotlinlogging.KotlinLogging
2328import io.micronaut.http.HttpRequest
2429import io.micronaut.http.context.ServerRequestContext
30+ import io.micronaut.security.utils.SecurityService
2531import jakarta.inject.Singleton
2632import java.util.UUID
2733
@@ -60,33 +66,81 @@ private val logger = KotlinLogging.logger {}
6066open class RoleResolver (
6167 private val authenticationHeaderResolver : AuthenticationHeaderResolver ,
6268 private val currentUserService : CurrentUserService ,
69+ private val securityService : SecurityService ? ,
6370 private val permissionHandler : PermissionHandler ,
71+ private val featureFlags : FeatureFlagClient ,
6472) {
65- inner class Request {
66- var authUserId: String? = null
73+ data class Subject (
74+ val id : String ,
75+ val type : TokenType ,
76+ )
77+
78+ fun newRequest () = Request ()
79+
80+ inner class Request internal constructor() {
81+ var subject: Subject ? = null
82+ var claimedRoles: Set <String >? = null
6783 val props: MutableMap <String , String > = mutableMapOf ()
84+ val orgs: MutableSet <UUID > = mutableSetOf ()
6885
6986 fun withCurrentUser () =
7087 apply {
71- authUserId = currentUserService.currentUser.authUserId
88+ subject = Subject ( currentUserService.currentUser.authUserId, TokenType . USER )
7289 }
7390
74- fun withAuthUserId (authUserId : String ) =
91+ fun withSubject (
92+ id : String ,
93+ type : TokenType ,
94+ ) = apply {
95+ this .subject = Subject (id, type)
96+ }
97+
98+ fun withCurrentAuthentication () =
7599 apply {
76- this .authUserId = authUserId
100+ // In community auth, where micronaut auth is disabled, the security service isn't available,
101+ // so we need to manually fall back to the default user.
102+ if (securityService == null ) {
103+ withSubject(DEFAULT_USER_ID .toString(), TokenType .USER )
104+ }
105+
106+ securityService?.authentication?.map { auth ->
107+ logger.debug { " Using current authentication object ${auth.name} ${auth.roles} ${auth.attributes} " }
108+ withClaims(auth.name, auth.attributes)
109+ }
77110 }
78111
79- fun withCurrentHttpRequest () =
112+ fun withClaims (
113+ sub : String ,
114+ claims : Map <String , Any >,
115+ ) = apply {
116+ // Figure out the subject type and ID.
117+ subject = Subject (sub, TokenType .fromClaims(claims))
118+
119+ // Some tokens have roles in the claims.
120+ // If the token does have roles in the claims, then those are the only roles
121+ // resolved by Request.roles().
122+ val roles = (claims[" roles" ] as ? List <* >)?.filterIsInstance<String >()
123+ if (roles != null ) {
124+ claimedRoles = roles.toSet()
125+ }
126+ }
127+
128+ fun withRefsFromCurrentHttpRequest () =
80129 apply {
81- ServerRequestContext .currentRequest<Any >().map { withHttpRequest (it) }
130+ ServerRequestContext .currentRequest<Any >().map { withRefsFromHttpRequest (it) }
82131 }
83132
84- fun withHttpRequest (req : HttpRequest <* >) =
133+ fun withRefsFromHttpRequest (req : HttpRequest <* >) =
85134 apply {
86135 val headers = req.headers.asMap(String ::class .java, String ::class .java)
87136 props.putAll(headers)
88137 }
89138
139+ fun withOrg (organizationId : UUID ) =
140+ apply {
141+ orgs.add(organizationId)
142+ }
143+
90144 fun withRef (
91145 key : AuthenticationId ,
92146 value : String ,
@@ -110,34 +164,86 @@ open class RoleResolver(
110164 * roles() resolves the request details into a set of available roles.
111165 */
112166 fun roles (): Set <String > {
113- logger.debug { " Resolving roles for authUserId $authUserId " }
167+ // these make null-checking cleaner
168+ val subject = subject
169+ var claimedRoles = claimedRoles
114170
115- try {
116- val user = authUserId
117- if (user.isNullOrBlank()) {
118- logger.debug { " Provided authUserId is null or blank, returning empty role set" }
119- return emptySet()
120- }
171+ // We'd like to transition away from tokens that hard-code roles in the claims,
172+ // and look them up per-request instead. This will allow for more centralized,
173+ // more consistent code for determining roles, and prevents bugs with having
174+ // changing the set of roles needed by a token.
175+ if (featureFlags.boolVariation(IgnoreTokenRoleClaims , TokenSubject (subject?.id ? : " unknown" ))) {
176+ claimedRoles = null
177+ }
178+
179+ // This helps when new roles are added to the set of admin roles.
180+ // "ADMIN" implies a set of other roles (see AuthRole.getInstanceAdminRoles()).
181+ // When a new role is added to that set, any existing tokens in the wild that
182+ // contain hard-coded roles will not have this new role. To deal with that,
183+ // we regenerate the set of admin roles here.
184+ //
185+ // TODO Hopefully, in the near future, we'll move away from having hard-coded roles
186+ // in tokens and *always* generate the roles dynamically during the request processing,
187+ // so that this is no longer needed.
188+ if (claimedRoles?.contains(AuthRoleConstants .ADMIN ) == true ) {
189+ claimedRoles = AuthRole .getInstanceAdminRoles()
190+ }
191+
192+ logger.debug { " Resolving roles for $subject " }
193+
194+ if (subject == null ) {
195+ logger.debug { " subject is null, returning empty role set" }
196+ return emptySet()
197+ }
198+ if (subject.id.isBlank()) {
199+ logger.debug { " subject.id is blank, returning empty role set" }
200+ return emptySet()
201+ }
202+ if (! claimedRoles.isNullOrEmpty()) {
203+ return claimedRoles
204+ }
121205
122- val workspaceIds = authenticationHeaderResolver.resolveWorkspace(props)?.toSet() ? : emptySet()
123- val organizationIds = authenticationHeaderResolver.resolveOrganization(props)?.toSet() ? : emptySet()
124- val authUserIds = authenticationHeaderResolver.resolveAuthUserIds(props) ? : emptySet()
125- val perms = permissionHandler.getPermissionsByAuthUserId(authUserId)
126- return resolveRoles(perms, user, workspaceIds, organizationIds, authUserIds)
206+ return try {
207+ when (subject.type) {
208+ // Certain token types have a hard-coded list of roles.
209+ TokenType .WORKLOAD_API -> setOf (AuthRoleConstants .DATAPLANE )
210+ TokenType .EMBEDDED_V1 -> setOf (AuthRoleConstants .EMBEDDED_END_USER )
211+ // Everything else resolves roles via the "permissions" table.
212+ TokenType .DATAPLANE_V1 , TokenType .SERVICE_ACCOUNT ->
213+ resolvePermissions(
214+ subject.id,
215+ permissionHandler.getPermissionsByServiceAccountId(UUID .fromString(subject.id)),
216+ )
217+ TokenType .USER -> resolvePermissions(subject.id, permissionHandler.getPermissionsByAuthUserId(subject.id))
218+ }
127219 } catch (e: Exception ) {
128- logger.error(e) { " Failed to resolve roles for authUserId $authUserId " }
220+ logger.error(e) { " Failed to resolve roles for $subject " }
129221 return emptySet()
130222 }
131223 }
132224
225+ private fun resolvePermissions (
226+ subjectId : String ,
227+ perms : List <Permission >,
228+ ): Set <String > {
229+ logger.debug { " Resolving permissions for $subject and $perms " }
230+
231+ val workspaceIds = authenticationHeaderResolver.resolveWorkspace(props)?.toSet() ? : emptySet()
232+ val resolvedOrgIds = authenticationHeaderResolver.resolveOrganization(props)?.toSet() ? : emptySet()
233+ val authUserIds = authenticationHeaderResolver.resolveAuthUserIds(props) ? : emptySet()
234+ val allOrgIds = orgs + resolvedOrgIds
235+
236+ return resolveRoles(perms, subjectId, workspaceIds, allOrgIds, authUserIds)
237+ }
238+
133239 /* *
134240 * requireRole checks whether the request has the given role available,
135241 * and if not it throws ForbiddenProblem.
136242 */
137243 fun requireRole (role : String ) {
138244 if (! roles().contains(role)) {
139245 throw ForbiddenProblem (
140- ProblemMessageData ().message(" User does not have the required $role permissions to access the resource(s)." ),
246+ ProblemMessageData ().message(" Caller does not have the required $role permissions to access the resource(s)." ),
141247 )
142248 }
143249 }
@@ -155,33 +261,46 @@ open class RoleResolver(
155261 */
156262 fun resolveRoles (
157263 perms : List <Permission >,
158- currentAuthUserId : String ,
264+ subjectId : String ,
159265 workspaceIds : Set <UUID >,
160266 organizationIds : Set <UUID >,
161267 authUserIds : Set <String >,
162268 ): Set <String > {
269+ logger.debug {
270+ " Resolving roles for $subjectId with perms=$perms workspaceIds=$workspaceIds organizationIds=$organizationIds authUserIds=$authUserIds "
271+ }
272+
163273 val roles = mutableSetOf (AuthRoleConstants .AUTHENTICATED_USER )
164274
165275 // The SELF role denotes that request refers to the current request's identity.
166276 //
167277 // This relies on the assumption that the AuthenticationHeaderResolver will only
168278 // ever resolve authUserIds for one user (who can have multiple authUserIds).
169279 // TODO Technically, that's a weak assumption and we should make that interface clearer.
170- if (authUserIds.contains(currentAuthUserId )) {
280+ if (authUserIds.contains(subjectId )) {
171281 roles.add(AuthRoleConstants .SELF )
172282 }
173283
174284 if (perms.any { it.permissionType == PermissionType .INSTANCE_ADMIN }) {
175285 roles.addAll(AuthRole .getInstanceAdminRoles())
176286 }
177287
288+ perms.filter { it.permissionType == PermissionType .DATAPLANE }.forEach {
289+ if (it.workspaceId == null && it.organizationId == null ) {
290+ roles.add(AuthRoleConstants .DATAPLANE )
291+ }
292+ }
293+
178294 determineWorkspaceRole(perms, workspaceIds)?.let {
179295 roles.addAll(impliedRoles(it))
180296 }
181297 determineOrganizationRole(perms, organizationIds)?.let {
182298 roles.addAll(impliedRoles(it))
183299 }
184300
301+ logger.debug {
302+ " Resolved roles for $subjectId with perms=$perms workspaceIds=$workspaceIds organizationIds=$organizationIds authUserIds=$authUserIds to $roles "
303+ }
185304 return roles
186305 }
187306}
0 commit comments