示例#1
0
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()
示例#2
0
    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,
            },
        )
示例#3
0
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,
        }