Expose Configuration Type Entities for Site-Wide Data

Created on 19 May 2023, over 1 year ago
Updated 4 August 2023, over 1 year ago

What is on the roadmap for exposing Configuration Entities?

The request is to expose config entities to GraphQL. The most basic example would be a Site Name (system.site.name).

For a more structured and elective set of data to expose, I propose including support for entities from a module like "Config Pages" and/or "Site Settings and Labels". Even "Fragments" could be a great single use type of reusable data exposed by GraphQL. (not to be confused w/ GQL Fragments)

These modules are great for including site-wide configuration entities. A typically use case would be to inject site config variables into Twig templates.

My current use case:
In a decoupled project I use Site Settings & Labels to provide fieldable global configuration to my Vue/Nuxt app via GraphQL. Content or settings a site manager enters once within Drupal but can be reused throughout the frontend app.

✨ Feature request
Status

Fixed

Version

2.0

Component

Code

Created by

πŸ‡ΊπŸ‡ΈUnited States brandonratz

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @brandonratz
  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    All cool things

    I'll take a look at these today, suss out whats required to get started

  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    Ok cool,
    All three are content field-able types, which makes integration really easy. You just need a plugin definition to extend your schema types out for it. It'll then... Just work.

    For each of these examples, pretend you have created your own custom module called "brandon_graphql_compose"

    For Config Page:

    Create a file: web/modules/custom/brandon_graphql_compose/src/Plugin/GraphQLCompose/EntityType/ConfigPage.php

    
    declare(strict_types=1);
    
    namespace Drupal\brandon_graphql_compose\Plugin\GraphQLCompose\EntityType;
    
    use Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeEntityTypeBase;
    
    /**
     * Define entity type.
     *
     * @GraphQLComposeEntityType(
     *   id = "config_pages",
     *   interfaces = { "Node" },
     *   prefix = "ConfigPage",
     *   base_fields = {
     *     "uuid" = {},
     *     "changed" = {},
     *     "status" = {},
     *     "context" = {},
     *     "title" = {
     *       "field_type" = "entity_label",
     *     }
     *   }
     * )
     */
    class ConfigPage extends GraphQLComposeEntityTypeBase {
    
    }
    

    Fragments...

    Create a file: web/modules/custom/brandon_graphql_compose/src/Plugin/GraphQLCompose/EntityType/Fragment.php

    
    declare(strict_types=1);
    
    namespace Drupal\brandon_graphql_compose\Plugin\GraphQLCompose\EntityType;
    
    use Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeEntityTypeBase;
    
    /**
     * Define entity type.
     *
     * @GraphQLComposeEntityType(
     *   id = "fragment",
     *   interfaces = { "Node" },
     *   prefix = "Fragment",
     *   base_fields = {
     *     "uuid" = {},
     *     "created" = {},
     *     "changed" = {},
     *     "status" = {},
     *     "title" = {
     *       "field_type" = "entity_label",
     *     }
     *   }
     * )
     */
    class Fragment extends GraphQLComposeEntityTypeBase {
    
    }
    

    Site Settings

    Create a file: web/modules/custom/brandon_graphql_compose/src/Plugin/GraphQLCompose/EntityType/SiteSettings.php

    
    declare(strict_types=1);
    
    namespace Drupal\brandon_graphql_compose\Plugin\GraphQLCompose\EntityType;
    
    use Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeEntityTypeBase;
    
    /**
     * Define entity type.
     *
     * @GraphQLComposeEntityType(
     *   id = "site_setting_entity",
     *   interfaces = { "Node" },
     *   prefix = "SiteSetting",
     *   base_fields = {
     *     "uuid" = {},
     *     "langcode" = {},
     *     "created" = {},
     *     "changed" = {},
     *     "status" = {},
     *     "fieldset" = {},
     *     "title" = {
     *       "field_type" = "entity_label",
     *     }
     *   }
     * )
     */
    class SiteSettings extends GraphQLComposeEntityTypeBase {
    
    }
    

    ---

    So you could create just a custom module to to these entity types, really easily.

    I don't really have a preference for one over the other, and I might sit on the integration for a bit, and just compare their uses with some colleagues -- But is that easy enough for you to get started with these modules today?

    The thing I'm not so sure on for these entity types is permissions. Have a play and let me know how it's working for you.

  • πŸ‡ΊπŸ‡ΈUnited States brandonratz

    @AlMunnings

    Great pointers here. I think we can integrate our needs with these code snippets.

    There are a lot of similarities between Config Pages and Site Settings & Labels. There are almost identical with slight implementation differences. I had challenges selecting between them myself.

    I like to think of Config Pages as a way to create configuration routes. And I consider SS&L to be a single screen with sections of configuration. I like how they expose these settings to Twig, etc.
    https://www.drupal.org/project/site_settings/issues/2980857#comment-13387097 β†’

  • Status changed to Postponed over 1 year ago
  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί
  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    Of the three, I'd lean towards site_setting_entity due to it having langcode support on the entity. It'll gel better with multisite builds in the gql queries.

  • πŸ‡ΊπŸ‡ΈUnited States jmolivas El Centro, CA

    Of those thee we mostly use site_setting when we have a need for data settings.

  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    Ok cool!
    So theres no harm in adding a basic extra entity type in the root package, if you don't have the entity type installed, it does nothing.

    I'll take a look into the permissions side of it, and roll into dev if no issue

  • Status changed to Active over 1 year ago
  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί
  • Assigned to almunnings
  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί
  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    Looking more at this tonight, I think we'll might need to contrib up the line a hook_query_TAG_alter for site_settings so edge connections don't leak potentially private data via entity query.

  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    Due to how entityQuery works,

    I'm kind of leaning towards this hook_query_TAG_alter to limit the results of exposed entities to work with the site_settings hook_ENTITY_TYPE_access function...

    Because it's a hook, we can probably roll it ourself. I'm just not comfortable leaving a gap like this with something like a "site setting" that administrators would assume is private unless permissions apply.

    /**
     * Implements hook_query_TAG_alter().
     *
     * Limit the site setting entities that are returned to the user based on
     * their permissions to access a specific site setting type.
     */
    function site_settings_type_permissions_query_site_setting_entity_access_alter(AlterableInterface $query) {
    
      // We're only interested in applying our access restrictions to
      // SELECT queries. hook_ENTITY_TYPE_access() can be used to apply
      // access control to 'update' and 'delete' operations.
      if (!($query instanceof SelectInterface)) {
        return;
      }
    
      $permissions = &drupal_static(__FUNCTION__, []);
    
      $account = $query->getMetaData('account') ?: \Drupal::currentUser();
      $uid = $account->id();
    
      if (!isset($permissions[$uid])) {
        $permissions[$uid] = [];
    
        $entity_type = \Drupal::entityTypeManager()->getDefinition('site_setting_entity');
        $entity_type_storage = \Drupal::entityTypeManager()->getStorage($entity_type->getBundleEntityType());
    
        // Get a list of all types for site_settings_entity.
        $entity_bundles = $entity_type_storage
          ->getQuery()
          ->accessCheck(FALSE)
          ->execute();
    
        $data_table = $entity_type->getDataTable();
        $bundle_key = $entity_type->getKey('bundle');
    
        foreach ($entity_bundles as $type) {
          if ($account->hasPermission('view published site setting entities') || $account->hasPermission('view published ' . $type . ' site setting entities')) {
            $permissions[$uid]["[$data_table].[$bundle_key]"][] = $type;
          }
        }
      }
    
      if (empty($permissions[$uid])) {
        $query->alwaysFalse();
      }
    
      foreach ($permissions[$uid] as $table_row => $types) {
        $query->condition($table_row, $types, 'IN');
      }
    }
    

    At least this way, the exposed site setting will pay attention to the view published site setting entities and view published ' . $type . ' site setting entities permissions, in it's own way, by limiting the entityQuery by site_setting type.

    But by adding the generic site_setting view permissions to a role, admins are exposing routes like https://my.site/admin/structure/site_setting_entity/1 - Which isn't ideal.

    ---

    So it's feeling like we need a custom resolver at this point, It doesn't really fit the pattern of content.

    Any thoughts on a structure and permissions we could implement?

  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    Ok,
    Had another idea.

    I just mutilate the connection a little.

      /**
       * Applies slicing and reordering to the result so that it can be transmitted.
       *
       * The result from the database may be out of order and have overfetched. When
       * returning edges or nodes, this needs to be compensated in the same way.
       * This function removes the overfetching and ensures the results are in the
       * requested order.
       */
      protected function getOrderedResult(): SyncPromise {
        return $this->getResult()
          ->then(function ($edges) {
            // This is probably against the spec, but we need SOME sort check.
            // If an entity doesn't implement hook_query_TAG_alter then we
            // can't guarantee the access check is applied.
            return array_filter($edges, function (Edge $edge) {
              $entity = $edge->getNode();
              return ($entity instanceof AccessibleInterface) ? $entity->access('view') : FALSE;
            });
          })
          ->then(function ($edges) {
            // To allow for pagination we over-fetch results by one above the limits
            // so we must fix that now.
            $edges = array_slice($edges, 0, $this->first ?? $this->last);
    
            if ($this->shouldReverseResultEdges()) {
              $edges = array_reverse($edges);
            }
    
            return $edges;
          });
      }
    

    This introduces:

            return array_filter($edges, function (Edge $edge) {
              $entity = $edge->getNode();
              return ($entity instanceof AccessibleInterface) ? $entity->access('view') : FALSE;
            });
    

    Which will run an access check on all rows of the connection check. And remove rows. The side effect of this, is if you're not using a hook_query_TAG_alter for the entity being retrieved, results could be sometimes 10 items, sometimes, 6, sometimes 1. Hypothetically. But its safer.

    @jmolivas thoughts?

  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    Ok. So I’ve closed up the security issue on develop. Pretty sure πŸ™Œ

    Now its more about a deeper integration with this site setting module.

    It’s a trickier integration because of how sensitive the data potentially is. I think its worthy of a submodule and its own hooks and modifications

  • πŸ‡ΊπŸ‡ΈUnited States jmolivas El Centro, CA

    Agree on adding this feature as submodule

  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    I've not forgotten this ticket.
    I'm just figuring out alternative ways to do what you request without integrating heavily into those modules.

    I've kind of landed on the idea of custom settings being added to the info query.

    I've got the data prototyped.
    Just need to hook it up in a resolver.

    It should satisfy the original request for arbitrary settings being added, and any further integration with other modules is the responsibility of the site builder, or warrants its own module. I cant justify fixing up security gaps in a third party module as a part of this module.

    Hopefully adding these settings gets the core requirement over the line.

  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    Ok,
    I think this'll do it.

    - Custom settings
    - Allow plaintext global token replacement on value
    - Allow for multiple with same field name
    - Type casting

    Site settings exposes tokens.
    You can create a custom setting with that token value in it.

    Bingo bango, the scope is done.

  • Status changed to Fixed over 1 year ago
  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί
  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί
  • Status changed to Fixed over 1 year ago
  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    This is now part of beta9.

    Thanks!

Production build 0.71.5 2024