class CatalogView(ContainerRegistryApiMixin, ListAPIView): """ Handles requests to the /v2/_catalog endpoint """ queryset = models.ContainerDistribution.objects.all().only("base_path") serializer_class = ContainerCatalogSerializer pagination_class = ContainerCatalogPagination access_policy_class = RegistryAccessPolicy() def get_queryset(self, *args, **kwargs): """Filter the queryset based on public repositories and assigned permissions.""" queryset = super().get_queryset() distribution_permission = "container.pull_containerdistribution" namespace_permission = "container.namespace_pull_containerdistribution" public_repositories = queryset.filter(private=False) repositories_by_distribution = get_objects_for_user( self.request.user, distribution_permission, queryset ) namespace_refs = queryset.values_list("namespace", flat=True) namespaces = models.ContainerNamespace.objects.filter(pk__in=namespace_refs) repositories_by_namespace = get_objects_for_user( self.request.user, namespace_permission, namespaces ) repositories_by_namespace = queryset.filter(namespace__in=repositories_by_namespace) accessible_repositories = repositories_by_distribution & repositories_by_namespace return (public_repositories | accessible_repositories).distinct()
def __init__(self, user, service, scope): """ Store class-wide variables and initialize a dictionary used for determining permissions. Args: user (django.contrib.auth.models.User): Requesting user. service (str): Name of the service access is granted to. scope (str): Scope of the resource that is to be accessed. """ self.user = user self.service = service self.scope = scope self.access_policy = RegistryAccessPolicy() self.actions_permissions = defaultdict( lambda: lambda *args: False, { "pull": self.has_pull_permissions, "push": self.has_push_permissions, "*": self.has_view_catalog_permissions, }, )
class AuthorizationService: """ A class responsible for generating and managing a Bearer token. This class represents a token server which manages and grants permissions according to a user's scope. """ ANONYMOUS_USER = "******" VALID_ACTIONS = ["pull", "push", "*"] def __init__(self, user, service, scope): """ Store class-wide variables and initialize a dictionary used for determining permissions. Args: user (django.contrib.auth.models.User): Requesting user. service (str): Name of the service access is granted to. scope (str): Scope of the resource that is to be accessed. """ self.user = user self.service = service self.scope = scope self.access_policy = RegistryAccessPolicy() self.actions_permissions = defaultdict( lambda: lambda *args: False, { "pull": self.has_pull_permissions, "push": self.has_push_permissions, "*": self.has_view_catalog_permissions, }, ) def generate_token(self): """ Generate a Bearer token. A signed JSON web token is generated in this method. The structure of the token is adjusted according the documentation https://docs.docker.com/registry/spec/auth/jwt/. Returns: dict: A newly generated Bearer token. """ with open(settings.PUBLIC_KEY_PATH, "rb") as public_key: kid = self.generate_kid_header(public_key.read()) current_datetime = datetime.now() access = self.determine_access() token_server = getattr(settings, "TOKEN_SERVER", "") claim_set = self.generate_claim_set( access=[access], audience=self.service, issued_at=int(current_datetime.timestamp()), issuer=token_server, subject=self.user.username, ) with open(settings.PRIVATE_KEY_PATH, "rb") as private_key: binary_token = jwt.encode( claim_set, private_key.read(), algorithm=settings.TOKEN_SIGNATURE_ALGORITHM, headers={"kid": kid}, ) token = binary_token.decode("utf8") current_datetime_utc = current_datetime.strftime( "%Y-%m-%dT%H:%M:%S.%fZ") return { "expires_in": TOKEN_EXPIRATION_TIME, "issued_at": current_datetime_utc, "token": token, } def generate_kid_header(self, public_key): """Generate kid header in a libtrust compatible format.""" decoded_key = self._convert_key_format_from_pem_to_der(public_key) truncated_sha256 = hashlib.sha256(decoded_key).hexdigest()[:30].encode( "utf8") encoded_base32 = base64.b32encode(truncated_sha256).decode("utf8") return self._split_into_encoded_groups(encoded_base32) def _convert_key_format_from_pem_to_der(self, public_key): key_in_pem_format = serialization.load_pem_public_key( public_key, default_backend()) key_in_der_format = key_in_pem_format.public_bytes( serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo) return key_in_der_format def _split_into_encoded_groups(self, encoded_base32): """Split encoded and truncated base32 into 12 groups separated by ':'.""" kid = encoded_base32[:4] for index, char in enumerate(encoded_base32[4:], start=0): if index % 4 == 0: kid += ":" + char else: kid += char return kid def determine_access(self): """ Determine access permissions for a corresponding user. This method determines whether the user has a valid access permission or not. The determination is based on role based access control. Returns: list: An intersected set of the requested and the allowed access. """ typ, name, actions = self.scope.split(":") actions = set(actions.split(",")) permitted_actions = set() if "push" in actions: actions.remove("push") has_permission = self.actions_permissions["push"](name) if has_permission: permitted_actions.add("push") permitted_actions.add("pull") actions.discard("pull") for action in actions: has_permission = self.actions_permissions[action](name) if has_permission: permitted_actions.add(action) return {"type": typ, "name": name, "actions": list(permitted_actions)} def has_permission(self, obj, method, action, data): """Check if user has permission to perform action.""" # Fake the request request = Request(HttpRequest()) request.method = method request.user = self.user request._full_data = data # Fake the corresponding view view = namedtuple("FakeView", ["action", "get_object"])(action, lambda: obj) return self.access_policy.has_permission(request, view) def has_pull_permissions(self, path): """ Check if the user has permissions to pull from the repository specified by the path. """ try: distribution = ContainerDistribution.objects.get(base_path=path) except ContainerDistribution.DoesNotExist: namespace_name = path.split("/")[0] try: namespace = ContainerNamespace.objects.get(name=namespace_name) except ContainerNamespace.DoesNotExist: # Check if user is allowed to create a new namespace return self.has_permission(None, "POST", "create", {"name": namespace_name}) # Check if user is allowed to view distributions in the namespace return self.has_permission(namespace, "GET", "view_distribution", {"name": namespace_name}) return self.has_permission(distribution, "GET", "pull", {"base_path": path}) def has_push_permissions(self, path): """ Check if the user has permissions to push to the repository specified by the path. """ try: distribution = ContainerDistribution.objects.get(base_path=path) except ContainerDistribution.DoesNotExist: namespace_name = path.split("/")[0] try: namespace = ContainerNamespace.objects.get(name=namespace_name) except ContainerNamespace.DoesNotExist: # Check if user is allowed to create a new namespace return self.has_permission(None, "POST", "create", {"name": namespace_name}) # Check if user is allowed to create a new distribution in the namespace return self.has_permission(namespace, "POST", "create_distribution", {}) return self.has_permission(distribution, "POST", "push", {"base_path": path}) def has_view_catalog_permissions(self, path): """ Check if the authenticated user is an administrator and is requesting the catalog endpoint. """ try: distribution = ContainerDistribution.objects.get(base_path=path) except ContainerDistribution.DoesNotExist: distribution = None # Fake the request request = Request(HttpRequest()) request.method = "GET" request.user = self.user request._full_data = {"base_path": path} # Fake the corresponding view view = namedtuple("FakeView", ["action", "get_object"])("catalog", lambda: distribution) return self.access_policy.has_permission(request, view) @staticmethod def generate_claim_set(issuer, issued_at, subject, audience, access): """ Generate the claim set that will be signed and dispatched back to the requesting subject. """ token_id = str(uuid.UUID(int=random.getrandbits(128), version=4)) expiration = issued_at + TOKEN_EXPIRATION_TIME return { "access": access, "aud": audience, "exp": expiration, "iat": issued_at, "iss": issuer, "jti": token_id, "nbf": issued_at, "sub": subject, }