@@ -20,10 +20,12 @@ import com.auth0.jwt.interfaces.DecodedJWT
20
20
import com.google.common.cache.CacheBuilder
21
21
import org.keycloak.authorization.client.AuthzClient
22
22
import org.keycloak.authorization.client.Configuration
23
+ import org.keycloak.authorization.client.resource.ProtectedResource
23
24
import org.keycloak.representations.idm.authorization.AuthorizationRequest
24
25
import org.keycloak.representations.idm.authorization.Permission
25
26
import org.keycloak.representations.idm.authorization.PermissionRequest
26
27
import org.keycloak.representations.idm.authorization.ResourceRepresentation
28
+ import org.keycloak.representations.idm.authorization.ScopeRepresentation
27
29
import java.net.URL
28
30
import java.util.concurrent.TimeUnit
29
31
@@ -52,9 +54,11 @@ object KeycloakUtils {
52
54
}
53
55
54
56
private val permissionCache = CacheBuilder .newBuilder()
55
- .expireAfterWrite(1 , TimeUnit .MINUTES )
57
+ .expireAfterWrite(30 , TimeUnit .SECONDS )
56
58
.build<String , List <Permission >>()
57
- private val existingResources = HashSet <String >()
59
+ private val existingResources = CacheBuilder .newBuilder()
60
+ .expireAfterWrite(3 , TimeUnit .MINUTES )
61
+ .build<String , ResourceRepresentation >()
58
62
59
63
private fun patchUrls (c : AuthzClient ): AuthzClient {
60
64
patchObject(c.serverConfiguration)
@@ -105,43 +109,115 @@ object KeycloakUtils {
105
109
}
106
110
107
111
@Synchronized
108
- fun hasPermission (accessToken : DecodedJWT , resourceName : String , scope : String ): Boolean {
109
- ensureResourcesExists(resourceName )
112
+ fun hasPermission (accessToken : DecodedJWT , resourceSpec : KeycloakResource , scope : KeycloakScope ): Boolean {
113
+ ensureResourcesExists(resourceSpec, accessToken )
110
114
val allPermissions = getPermissions(accessToken)
111
- val forResource = allPermissions.filter { it.resourceName == resourceName }
115
+ val forResource = allPermissions.filter { it.resourceName == resourceSpec.name }
112
116
if (forResource.isEmpty()) return false
113
117
val scopes: Set <String > = forResource.mapNotNull { it.scopes }.flatten().toSet()
114
118
if (scopes.isEmpty()) {
115
119
// If the permissions are not restricted to any scope we assume they are valid for all scopes.
116
120
return true
117
121
}
118
- return scopes.contains(scope)
122
+ return scopes.contains(scope.name )
119
123
}
120
124
121
- private fun hasPermissions (accessToken : DecodedJWT , resourceNames : List <String >): Boolean {
122
- try {
123
- val ticket = authzClient.protection(accessToken.token).permission().create(resourceNames.map { PermissionRequest (it) }).ticket
124
- val rpt = authzClient.authorization(accessToken.token).authorize(AuthorizationRequest (ticket)).token
125
- val introspect = authzClient.protection().introspectRequestingPartyToken(rpt)
126
- return introspect.active
127
- } catch (e: Exception ) {
128
- throw RuntimeException (" Can't get permissions for token: ${accessToken.token} " , e)
129
- }
130
- }
125
+ @Synchronized
126
+ fun grantPermission (user : DecodedJWT , resourceSpec : KeycloakResource , scopes : Set <KeycloakScope >): Boolean {
127
+ val resource = ensureResourcesExists(resourceSpec, user)
128
+ val ticketResponse = authzClient.protection(user.token).permission()
129
+ .create(PermissionRequest (resource.id, * scopes.map { it.name }.toTypedArray()))
131
130
132
- private fun createPermission ( ownerToken : DecodedJWT ? , resourceName : String ): String {
133
- val protection = if (ownerToken == null ) authzClient.protection() else authzClient.protection(ownerToken.token )
134
- val newResource = protection.resource().create( ResourceRepresentation (resourceName))
135
- return newResource.id
131
+ val authResponse = authzClient.authorization( /* service account */ )
132
+ .authorize( AuthorizationRequest (ticketResponse.ticket) )
133
+
134
+ return authResponse.isUpgraded
136
135
}
137
136
138
137
@Synchronized
139
- fun ensureResourcesExists (resourceName : String ) {
140
- if (existingResources.contains(resourceName)) return
141
- if (authzClient.protection().resource().findByName(resourceName) == null ) {
142
- authzClient.protection().resource().create(ResourceRepresentation (resourceName))
138
+ fun ensureResourcesExists (
139
+ resourceSpec : KeycloakResource ,
140
+ owner : DecodedJWT ? = null
141
+ ): ResourceRepresentation {
142
+ return existingResources.get(resourceSpec.name) {
143
+ var resource = authzClient.protection().resource().findByNameAnyOwner(resourceSpec.name)
144
+ if (resource != null ) return @get resource
145
+ val protection = (
146
+ if (resourceSpec.type.ownerManaged) {
147
+ owner?.let { authzClient.protection(owner.token) }
148
+ } else {
149
+ null
150
+ }
151
+ ) ? : authzClient.protection()
152
+ resource = ResourceRepresentation ().apply {
153
+ name = resourceSpec.name
154
+ scopes = resourceSpec.type.scopes.map { ScopeRepresentation (it.name) }.toSet()
155
+ type = resourceSpec.type.name
156
+ if (resourceSpec.type.ownerManaged) ownerManagedAccess = true
157
+ }
158
+ resource = protection.resource().create(resource)
143
159
permissionCache.invalidateAll()
160
+ return @get resource
144
161
}
145
- existingResources + = resourceName
162
+
163
+ }
164
+ }
165
+
166
+ data class KeycloakScope (val name : String ) {
167
+ operator fun plus (other : KeycloakScope ): Set <KeycloakScope > = setOf (this , other)
168
+ fun toSet () = setOf (this )
169
+
170
+ companion object {
171
+ val ADD = KeycloakScope (" add" ) // the user can add a new item, but not remove other items in a list
172
+ val LIST = KeycloakScope (" list" ) // the user can see that an item exists, but not read the contents
173
+ val READ = KeycloakScope (" read" )
174
+ val WRITE = KeycloakScope (" write" )
175
+ val DELETE = KeycloakScope (" delete" )
176
+ val READ_WRITE_DELETE = setOf (READ , WRITE , DELETE )
177
+ val READ_WRITE_DELETE_LIST = setOf (READ , WRITE , DELETE , LIST )
178
+ val READ_WRITE = setOf (READ , WRITE )
179
+ val READ_WRITE_LIST = setOf (READ , WRITE , LIST )
180
+ val READ_ONLY = setOf (READ )
181
+ val READ_LIST = setOf (READ , LIST )
182
+ val ALL_SCOPES = READ_WRITE_DELETE_LIST
183
+ }
184
+ }
185
+ fun EPermissionType.toKeycloakScope (): KeycloakScope = when (this ) {
186
+ EPermissionType .READ -> KeycloakScope .READ
187
+ EPermissionType .WRITE -> KeycloakScope .WRITE
188
+ }
189
+
190
+ data class KeycloakResource (val name : String , val type : KeycloakResourceType ) {
191
+
192
+ }
193
+
194
+ data class KeycloakResourceType (val name : String , val scopes : Set <KeycloakScope >, val ownerManaged : Boolean = false ) {
195
+ fun createInstance (resourceName : String ) = KeycloakResource (this .name + " /" + resourceName, this )
196
+
197
+ companion object {
198
+ val DEFAULT_TYPE = KeycloakResourceType (" default" , KeycloakScope .READ_WRITE )
199
+ val MODEL_SERVER_ENTRY = KeycloakResourceType (" model-server-entry" , KeycloakScope .READ_WRITE_DELETE )
200
+ val REPOSITORY = KeycloakResourceType (" repository" , KeycloakScope .READ_WRITE_DELETE_LIST )
201
+ val WORKSPACE = KeycloakResourceType (" workspace" , KeycloakScope .READ_WRITE_DELETE_LIST , ownerManaged = true )
146
202
}
203
+ }
204
+
205
+ fun String.asResource () = KeycloakResourceType .DEFAULT_TYPE .createInstance(this )
206
+
207
+ private fun ProtectedResource.findByNameAnyOwner (name : String ): ResourceRepresentation ? {
208
+ val resources: List <ResourceRepresentation > = org.modelix.authorization.KeycloakUtils .authzClient.protection().resource()
209
+ .find(
210
+ null ,
211
+ name,
212
+ null ,
213
+ null ,
214
+ null ,
215
+ null ,
216
+ false ,
217
+ true ,
218
+ true ,
219
+ null ,
220
+ null
221
+ )
222
+ return resources.firstOrNull()
147
223
}
0 commit comments