Skip to content

Commit

Permalink
Merge pull request #8 from alexanderwolz/enhance-and-cleanup-tests
Browse files Browse the repository at this point in the history
added new tests and cleaned up code
  • Loading branch information
alexanderwolz authored May 19, 2023
2 parents 8ffb038 + 59e9150 commit ffca7d9
Show file tree
Hide file tree
Showing 12 changed files with 689 additions and 518 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
![GitHub](https://img.shields.io/badge/keycloak-21.1.1-orange)
![GitHub](https://img.shields.io/badge/registry-2.8.2-orange)
![GitHub](https://img.shields.io/github/license/alexanderwolz/keycloak-docker-group-role-mapper)
![GitHub](https://img.shields.io/badge/test_cases-604-informational)
![GitHub](https://img.shields.io/badge/test_cases-624-informational)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/alexanderwolz/keycloak-docker-group-role-mapper)
![GitHub all releases](https://img.shields.io/github/downloads/alexanderwolz/keycloak-docker-group-role-mapper/total?color=informational)

Expand All @@ -31,10 +31,10 @@ See also Keycloak [Dockerfile](https://github.com/alexanderwolz/keycloak-docker-
## ⚙️ Configuration
This mapper supports following environment variables (either set on server or in docker container):

| Variable Name | Values | Description |
|---------------------------------|------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ```REGISTRY_CATALOG_AUDIENCE``` | ```editor```, ```user``` | Will allow editors or users to access *registry:catalog:** scope. That would be of interest to users who want to access UI frontends.<br> No scope is set by default, so only admins are allowed to access registry scope. |
| ```REGISTRY_NAMESPACE_SCOPE``` | ```group```, ```domain```, ```sld```, ```username``` | If ```username``` is set, users will be granted full access to the namespace if it matches their username (lowercase check).<br>If ```domain``` is set, users are checked against their email domain and will be granted access to the repository (e.g. *company.com/image*) according to their roles.<br>If ```sld``` is set, users are checked against their email second level domain (sld) and will be granted access to the repository (e.g. *company/image*) according to their roles.<br>If ```group``` is set, users are checked for group membership and will be granted access to the repository according to their roles.<br>Namespace scope ```group``` is set by default or if value is empty or no value matches ```username```, ```domain```, ```sld``` or ```group```.<br>All values can be concatenated with ```,```. |
| Variable Name | Values | Description |
|---------------------------------|------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ```REGISTRY_CATALOG_AUDIENCE``` | ```editor```, ```user``` | Will allow editors or users to access *registry:catalog:** scope. That would be of interest to users who want to access UI frontends.<br> No scope is set by default, so only admins are allowed to access registry scope. |
| ```REGISTRY_NAMESPACE_SCOPE``` | ```group```, ```domain```, ```sld```, ```username``` | If ```group``` is set, users are checked for group membership and will be granted access to the repository according to their roles.<br>If ```domain``` is set, users are checked against their email domain and will be granted access to the repository (e.g. *company.com/image*) according to their roles.<br>If ```sld``` is set, users are checked against their email second level domain (sld) and will be granted access to the repository (e.g. *company/image*) according to their roles.<br>If ```username``` is set, users will be granted full access to the namespace if it matches their username (lowercase check).<br><br>Namespace scope ```group``` is set by default or if value is empty or no value matches ```group```, ```domain```, ```sld``` or ```username``` (all values can be concatenated with ```,```). |


## 🔒 Keycloak Setup
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
}

group = 'de.alexanderwolz'
version = '1.3.0'
version = '1.3.1'

repositories {
mavenCentral()
Expand Down
2 changes: 1 addition & 1 deletion examples/keycloak-with-mapper/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
ARG ALPINE_VERSION="3.18.0"
ARG KEYCLOAK_VERSION="21.1.1"
ARG MAPPER_VERSION="1.3.0"
ARG MAPPER_VERSION="1.3.1"
ARG MAPPER_LIB="keycloak-docker-group-role-mapper-${MAPPER_VERSION}.jar"

# stage 1: mapper build
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package de.alexanderwolz.keycloak.docker.mapping

import org.jboss.logging.Logger
import org.keycloak.models.AuthenticatedClientSessionModel
import org.keycloak.models.ClientModel
import org.keycloak.models.UserModel
import org.keycloak.protocol.docker.DockerAuthV2Protocol
import org.keycloak.protocol.docker.mapper.DockerAuthV2ProtocolMapper
import org.keycloak.representations.docker.DockerAccess
import org.keycloak.representations.docker.DockerResponseToken

abstract class AbstractDockerScopeMapper(
private val id: String,
private val displayType: String,
private val helpText: String
) : DockerAuthV2ProtocolMapper() {

companion object {
const val NAME_CATALOG = "catalog"

const val ACTION_PULL = "pull"
const val ACTION_PUSH = "push"
const val ACTION_DELETE = "delete"
const val ACTION_ALL = "*"
val ACTION_ALL_SUBSTITUTE = listOf(ACTION_PULL, ACTION_PUSH, ACTION_DELETE)

//see also https://docs.docker.com/registry/spec/auth/scope/
const val ACCESS_TYPE_REGISTRY = "registry"
const val ACCESS_TYPE_REPOSITORY = "repository"
const val ACCESS_TYPE_REPOSITORY_PLUGIN = "repository(plugin)"
}

internal val logger: Logger = Logger.getLogger(javaClass.simpleName)

override fun getId(): String {
return id
}

override fun getDisplayType(): String {
return displayType
}

override fun getHelpText(): String {
return helpText
}

internal fun getScopesFromSession(clientSession: AuthenticatedClientSessionModel): Collection<String> {
val scopeString = clientSession.getNote(DockerAuthV2Protocol.SCOPE_PARAM)
if (logger.isDebugEnabled && (scopeString == null || scopeString.isEmpty())) {
logger.debug("Session does not contain a scope, ignoring further access check")
}
return scopeString?.split(" ") ?: emptySet()
}

internal fun parseScopeIntoAccessItem(scope: String): DockerScopeAccess? {
return try {
val accessItem = DockerScopeAccess(scope)
if (logger.isTraceEnabled) {
logger.trace("Parsed scope '$scope' into: $accessItem")
}
accessItem
} catch (e: Exception) {
logger.warn("Could not parse scope '$scope' into access object", e)
null
}
}

internal fun getClientRoleNames(user: UserModel, client: ClientModel): Collection<String> {
return user.getClientRoleMappingsStream(client).map { it.name }.toList()
}

internal fun getDomainFromEmail(email: String): String? {
val parts = email.split("@")
if (parts.size == 2) {
val domain = parts.last()
if (domain.isNotEmpty()) {
return domain
}
}
return null //no valid domain
}

internal fun getSecondLevelDomainFromEmail(email: String): String? {
val domain = getDomainFromEmail(email) ?: return null
val parts = domain.split(".")
if (parts.size > 1) {
val sld = parts[parts.size - 2]
if (sld.isNotEmpty()) {
return sld
}
}
return null
}

internal fun isUsernameRepository(namespace: String, username: String): Boolean {
return namespace == username.lowercase()
}

internal fun isDomainRepository(namespace: String, email: String): Boolean {
return namespace == getDomainFromEmail(email)
}

internal fun isSecondLevelDomainRepository(namespace: String, email: String): Boolean {
return namespace == getSecondLevelDomainFromEmail(email)
}

internal fun hasAllPrivileges(actions: Collection<String>, requestedActions: Collection<String>): Boolean {
return isSubstituteWithActionAll(actions, requestedActions) || actions.containsAll(requestedActions)
}

internal fun isSubstituteWithActionAll(
actions: Collection<String>, requestedActions: Collection<String>
): Boolean {
return requestedActions.size == 1 && requestedActions.first() == ACTION_ALL && actions.containsAll(
ACTION_ALL_SUBSTITUTE
)
}

internal fun getNamespaceFromRepositoryName(repositoryName: String): String? {
val parts = repositoryName.split("/")
if (parts.size == 2) {
return parts[0].lowercase()
}
return null
}

internal fun substituteRequestedActions(requestedActions: Collection<String>): List<String> {
// replaces '*' by pull, push and delete
return HashSet(requestedActions).also { actions ->
if (actions.contains(ACTION_ALL)) {
actions.remove(ACTION_ALL)
actions.addAll(ACTION_ALL_SUBSTITUTE)
}
}.toList()
}

internal fun allowAll(
responseToken: DockerResponseToken,
accessItem: DockerScopeAccess,
user: UserModel,
reason: String
): DockerResponseToken {
if (logger.isDebugEnabled) {
logger.debug("Granting access for user '${user.username}' on scope '${accessItem.scope}': $reason")
}
responseToken.accessItems.add(accessItem)
return responseToken
}

internal fun allowWithActions(
responseToken: DockerResponseToken,
accessItem: DockerScopeAccess,
allowedActions: List<String>,
user: UserModel,
reason: String
): DockerResponseToken {
if (logger.isDebugEnabled) {
logger.debug("Granting access for user '${user.username}' on scope '${accessItem.scope}': $reason")
}
accessItem.actions = allowedActions
responseToken.accessItems.add(accessItem)
return responseToken
}

internal fun deny(
responseToken: DockerResponseToken,
accessItem: DockerScopeAccess,
user: UserModel,
reason: String
): DockerResponseToken {
logger.warn("Access denied for user '${user.username}' on scope '${accessItem.scope}': $reason")
return responseToken
}

internal fun getEnvVariable(key: String): String? {
return try {
System.getenv()[key]
} catch (e: Exception) {
logger.error("Could not access System Environment", e)
null
}
}

// cache plain scope into DockerAccess class
internal class DockerScopeAccess(val scope: String) : DockerAccess(scope)

}
Loading

0 comments on commit ffca7d9

Please sign in to comment.