Skip to content

Commit e7ba7c4

Browse files
committed
ASU-1793: Fix default consumer and oauth keys
- oauth keys now get 600 perms - default oauth consumer now gets a user created for it automatically
1 parent 36a4a91 commit e7ba7c4

3 files changed

Lines changed: 213 additions & 15 deletions

File tree

docker/openshift/init.sh

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,38 @@ if [ -n "${DRUPAL_SIMPLE_OAUTH_PRIVATE_KEY_PEM}" ] || [ -n "${DRUPAL_SIMPLE_OAUT
1010
key_dir="${DRUPAL_SIMPLE_OAUTH_KEY_DIR:-${private_files_dir%/}/simple_oauth}"
1111
private_key_path="${key_dir%/}/private.key"
1212
public_key_path="${key_dir%/}/public.key"
13+
old_umask="$(umask)"
1314

14-
if { [ -n "${DRUPAL_SIMPLE_OAUTH_PRIVATE_KEY_PEM}" ] && [ ! -f "${private_key_path}" ]; } || { [ -n "${DRUPAL_SIMPLE_OAUTH_PUBLIC_KEY_PEM}" ] && [ ! -f "${public_key_path}" ]; }; then
15-
if ! mkdir -p "${key_dir}"; then
16-
echo "Container start error: Unable to create Simple OAuth key directory at '${key_dir}'."
17-
exit 1
18-
fi
19-
chmod 700 "${key_dir}" || true
15+
if ! mkdir -p "${key_dir}"; then
16+
echo "Container start error: Unable to create Simple OAuth key directory at '${key_dir}'."
17+
exit 1
18+
fi
19+
chmod 700 "${key_dir}" || true
2020

21-
if [ -n "${DRUPAL_SIMPLE_OAUTH_PRIVATE_KEY_PEM}" ] && [ ! -f "${private_key_path}" ]; then
22-
printf '%s\n' "${DRUPAL_SIMPLE_OAUTH_PRIVATE_KEY_PEM}" > "${private_key_path}"
21+
# Create files with correct permissions even when chmod is blocked.
22+
umask 077
23+
24+
if [ -n "${DRUPAL_SIMPLE_OAUTH_PRIVATE_KEY_PEM}" ]; then
25+
if [ -f "${private_key_path}" ] && ! chmod 600 "${private_key_path}" 2>/dev/null; then
26+
rm -f "${private_key_path}" || true
27+
fi
28+
if [ ! -f "${private_key_path}" ]; then
29+
printf '%b\n' "${DRUPAL_SIMPLE_OAUTH_PRIVATE_KEY_PEM}" > "${private_key_path}"
2330
chmod 600 "${private_key_path}" || true
2431
fi
32+
fi
2533

26-
if [ -n "${DRUPAL_SIMPLE_OAUTH_PUBLIC_KEY_PEM}" ] && [ ! -f "${public_key_path}" ]; then
27-
printf '%s\n' "${DRUPAL_SIMPLE_OAUTH_PUBLIC_KEY_PEM}" > "${public_key_path}"
34+
if [ -n "${DRUPAL_SIMPLE_OAUTH_PUBLIC_KEY_PEM}" ]; then
35+
if [ -f "${public_key_path}" ] && ! chmod 600 "${public_key_path}" 2>/dev/null; then
36+
rm -f "${public_key_path}" || true
37+
fi
38+
if [ ! -f "${public_key_path}" ]; then
39+
printf '%b\n' "${DRUPAL_SIMPLE_OAUTH_PUBLIC_KEY_PEM}" > "${public_key_path}"
2840
chmod 600 "${public_key_path}" || true
2941
fi
3042
fi
43+
44+
umask "${old_umask}" || true
3145
fi
3246

3347
function get_deploy_id {

public/modules/custom/asu_rest/asu_rest.install

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,24 +69,113 @@ function asu_rest_provision_oauth_consumer(): void {
6969
}
7070

7171
$storage = \Drupal::entityTypeManager()->getStorage('consumer');
72-
$existing = $storage->getQuery()
72+
$existing_ids = $storage->getQuery()
7373
->accessCheck(FALSE)
7474
->condition('client_id', $client_id)
7575
->range(0, 1)
7676
->execute();
77-
if ($existing) {
77+
if ($existing_ids) {
78+
// Ensure default user exists and is assigned, even if consumer already
79+
// exists (common when secrets are added after initial install).
80+
$role_storage = \Drupal::entityTypeManager()->getStorage('user_role');
81+
if (!$role_storage->load('rest_client')) {
82+
\Drupal::logger('asu_rest')->error(
83+
'User role rest_client is missing; cannot provision OAuth REST client user for consumer @id.',
84+
['@id' => $client_id],
85+
);
86+
return;
87+
}
88+
$username_env = getenv('ASU_REST_OAUTH_DEFAULT_USERNAME');
89+
$username = is_string($username_env) && trim($username_env) !== '' ? trim($username_env) : 'rest_client';
90+
$user_storage = \Drupal::entityTypeManager()->getStorage('user');
91+
$users = $user_storage->loadByProperties(['name' => $username]);
92+
/** @var \Drupal\user\Entity\User|null $user */
93+
$user = $users ? reset($users) : NULL;
94+
if (!$user) {
95+
$password = bin2hex(random_bytes(16));
96+
$user = $user_storage->create([
97+
'name' => $username,
98+
'status' => 1,
99+
'roles' => ['rest_client'],
100+
'pass' => $password,
101+
]);
102+
$user->save();
103+
}
104+
else {
105+
if (!$user->isActive()) {
106+
$user->activate();
107+
}
108+
if (!in_array('rest_client', $user->getRoles(), TRUE)) {
109+
$user->addRole('rest_client');
110+
}
111+
$user->save();
112+
}
113+
114+
/** @var \Drupal\consumers\Entity\Consumer|null $existing_consumer */
115+
$existing_consumer = $storage->load(reset($existing_ids));
116+
if ($existing_consumer && $existing_consumer->get('user_id')->isEmpty()) {
117+
$existing_consumer->set('user_id', (int) $user->id())->save();
118+
\Drupal::logger('asu_rest')->notice(
119+
'Updated OAuth consumer @id default user to uid @uid.',
120+
['@id' => $client_id, '@uid' => $user->id()],
121+
);
122+
}
78123
return;
79124
}
80125

81-
$scope_storage = \Drupal::entityTypeManager()->getStorage('oauth2_scope');
82-
if (!$scope_storage->load('rest_client')) {
126+
$entity_type_manager = \Drupal::entityTypeManager();
127+
// Some environments provide scopes as config entities (oauth2_scope). If the
128+
// entity type exists, ensure the expected scope is present.
129+
if ($entity_type_manager->hasDefinition('oauth2_scope')) {
130+
$scope_storage = $entity_type_manager->getStorage('oauth2_scope');
131+
if (!$scope_storage->load('rest_client')) {
132+
\Drupal::logger('asu_rest')->error(
133+
'OAuth scope rest_client is missing; cannot create consumer @id.',
134+
['@id' => $client_id],
135+
);
136+
return;
137+
}
138+
}
139+
140+
$role_storage = \Drupal::entityTypeManager()->getStorage('user_role');
141+
if (!$role_storage->load('rest_client')) {
83142
\Drupal::logger('asu_rest')->error(
84-
'OAuth scope rest_client is missing; cannot create consumer @id.',
143+
'User role rest_client is missing; cannot provision OAuth REST client user for consumer @id.',
85144
['@id' => $client_id],
86145
);
87146
return;
88147
}
89148

149+
$username_env = getenv('ASU_REST_OAUTH_DEFAULT_USERNAME');
150+
$username = is_string($username_env) && trim($username_env) !== '' ? trim($username_env) : 'rest_client';
151+
$user_storage = \Drupal::entityTypeManager()->getStorage('user');
152+
$users = $user_storage->loadByProperties(['name' => $username]);
153+
/** @var \Drupal\user\Entity\User|null $user */
154+
$user = $users ? reset($users) : NULL;
155+
if (!$user) {
156+
$password = bin2hex(random_bytes(16));
157+
$user = $user_storage->create([
158+
'name' => $username,
159+
'status' => 1,
160+
'roles' => ['rest_client'],
161+
'pass' => $password,
162+
]);
163+
$user->save();
164+
\Drupal::logger('asu_rest')->notice(
165+
'Created OAuth REST client user @name (uid @uid) for consumer @id.',
166+
['@name' => $username, '@uid' => $user->id(), '@id' => $client_id],
167+
);
168+
}
169+
else {
170+
if (!$user->isActive()) {
171+
$user->activate();
172+
}
173+
if (!in_array('rest_client', $user->getRoles(), TRUE)) {
174+
$user->addRole('rest_client');
175+
}
176+
$user->save();
177+
}
178+
90179
$label = $oauth['label'] ?? 'Apartment application service';
91180
$label = is_string($label) && $label !== '' ? $label : 'Apartment application service';
92181

@@ -96,6 +185,7 @@ function asu_rest_provision_oauth_consumer(): void {
96185
'description' => 'Machine client for Elasticsearch-compatible REST endpoints (projects, apartments).',
97186
'grant_types' => ['client_credentials'],
98187
'scopes' => ['rest_client'],
188+
'user_id' => (int) $user->id(),
99189
'confidential' => TRUE,
100190
'secret' => $secret,
101191
'redirect' => ['https://127.0.0.1/oauth2-callback-not-used'],
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Drupal\Tests\asu_rest\Kernel;
6+
7+
use Drupal\consumers\Entity\Consumer;
8+
use Drupal\KernelTests\KernelTestBase;
9+
use Drupal\user\Entity\Role;
10+
use Drupal\user\Entity\User;
11+
12+
/**
13+
* Tests OAuth consumer provisioning for REST API clients.
14+
*
15+
* @group asu_rest
16+
*/
17+
final class OAuthConsumerProvisioningTest extends KernelTestBase {
18+
19+
/**
20+
* {@inheritdoc}
21+
*/
22+
protected static $modules = [
23+
'system',
24+
'user',
25+
'file',
26+
'image',
27+
'serialization',
28+
'rest',
29+
'consumers',
30+
'simple_oauth',
31+
'asu_rest',
32+
];
33+
34+
protected function setUp(): void {
35+
parent::setUp();
36+
37+
$this->installEntitySchema('user');
38+
$this->installEntitySchema('consumer');
39+
40+
// Needed by consumers/simple_oauth entities.
41+
$this->installConfig(['system', 'user']);
42+
43+
// Role expected to exist via CMI in real environments.
44+
Role::create([
45+
'id' => 'rest_client',
46+
'label' => 'REST api client',
47+
])->save();
48+
}
49+
50+
public function tearDown(): void {
51+
putenv('ASU_REST_OAUTH_CLIENT_SECRET');
52+
putenv('ASU_REST_OAUTH_CLIENT_ID');
53+
putenv('ASU_REST_OAUTH_DEFAULT_USERNAME');
54+
parent::tearDown();
55+
}
56+
57+
/**
58+
* Provisioning creates a REST client user and assigns it to the consumer.
59+
*/
60+
public function testProvisioningCreatesUserAndAssignsConsumer(): void {
61+
putenv('ASU_REST_OAUTH_CLIENT_SECRET=test-secret');
62+
putenv('ASU_REST_OAUTH_DEFAULT_USERNAME=rest_client');
63+
64+
$this->config('asu_rest.settings')
65+
->set('oauth_consumer', [
66+
'auto_create' => TRUE,
67+
'client_id' => 'apartment_application_service',
68+
'label' => 'Apartment application service',
69+
])
70+
->save();
71+
72+
$this->container->get('module_handler')->loadInclude('asu_rest', 'install');
73+
call_user_func('asu_rest_provision_oauth_consumer');
74+
75+
$users = $this->container->get('entity_type.manager')
76+
->getStorage('user')
77+
->loadByProperties(['name' => 'rest_client']);
78+
/** @var \Drupal\user\Entity\User $user */
79+
$user = reset($users);
80+
$this->assertInstanceOf(User::class, $user);
81+
$this->assertTrue($user->isActive());
82+
$this->assertTrue(in_array('rest_client', $user->getRoles(), TRUE));
83+
84+
$consumers = $this->container->get('entity_type.manager')
85+
->getStorage('consumer')
86+
->loadByProperties(['client_id' => 'apartment_application_service']);
87+
/** @var \Drupal\consumers\Entity\Consumer $consumer */
88+
$consumer = reset($consumers);
89+
$this->assertInstanceOf(Consumer::class, $consumer);
90+
$this->assertSame((int) $user->id(), (int) $consumer->get('user_id')->target_id);
91+
}
92+
93+
}
94+

0 commit comments

Comments
 (0)