Edit clients

This commit is contained in:
Andreas Svanberg 2025-03-28 13:27:06 +01:00
parent a4f99f1b29
commit c9559ca930
Signed by: ansv7779
GPG Key ID: 729B051CFFD42F92
9 changed files with 180 additions and 2 deletions

@ -49,6 +49,12 @@ public class EmbeddedConfiguration {
clientRows.add(clientRow);
}
@Override
public void update(final ClientRow clientRow) {
clientRows.removeIf(existing -> existing.id().equals(clientRow.id()));
clientRows.add(clientRow);
}
@Override
public List<ClientRow> getClients(final Principal owner) {
return List.copyOf(clientRows);

@ -64,11 +64,23 @@ public class JDBCClientRepository implements ClientRepository {
getJdbc().sql("""
INSERT INTO v2_client (id, client_id, client_secret, name, redirect_uri, contact_email, scopes)
VALUES (:id, :clientId, :clientSecret, :name, :redirectUri, :contactEmail, :scopes)
ON DUPLICATE KEY UPDATE
client_id = VALUES(client_id),
client_secret = VALUES(client_secret),
name = VALUES(name),
redirect_uri = VALUES(redirect_uri),
contact_email = VALUES(contact_email),
scopes = VALUES(scopes);
""")
.paramSource(clientRow)
.update();
}
@Override
public void update(final ClientRow clientRow) {
addNewClient(clientRow);
}
@Override
public List<String> getOwners(final String id) {
return getJdbc().sql("SELECT owner FROM v2_client_owner WHERE client_id = :clientId")

@ -7,6 +7,8 @@ import java.util.Optional;
public interface ClientManagementService {
NewClient createClient(Principal owner, ClientData clientData);
Client updateClient(Principal principal, String id, ClientData clientData);
Optional<Client> getClient(Principal owner, String id);
List<Client> getClients(Principal owner);

@ -40,8 +40,8 @@ public class ClientManager implements RegisteredClientRepository, ClientManageme
String clientId = Util.generateAlphanumericString(16); // OAuth 2 client id
String clientSecret = clientData.isPublic() ? null : Util.generateAlphanumericString(32);
String encodedClientSecret = clientSecret == null ? null : passwordEncoder.encode(clientSecret);
String redirectURI = clientData.redirectURI() != null ? clientData.redirectURI().toString() : null;
String scopeString = String.join(" ", clientData.scopes());
String redirectURI = getRedirectUri(clientData);
String scopeString = toScopeString(clientData);
ClientRow clientRow = new ClientRow(id, clientId, clientData.clientName(), clientData.contactEmail(),
redirectURI, scopeString, encodedClientSecret);
@ -53,6 +53,32 @@ public class ClientManager implements RegisteredClientRepository, ClientManageme
return new NewClient(id, clientId, clientSecret);
}
@Override
public Client updateClient(final Principal principal, final String id, final ClientData clientData) {
boolean ownsClient = clientRepository.getOwners(id)
.contains(principal.getName());
if (!ownsClient) {
throw new IllegalStateException(principal.getName() + " is not an owner of the client");
}
ClientRow currentClient = clientRepository.getClientRowById(id)
.orElseThrow(() -> new IllegalArgumentException("No such client"));
ClientRow updated = new ClientRow(id, currentClient.clientId(), clientData.clientName(),
clientData.contactEmail(), getRedirectUri(clientData),
toScopeString(clientData), currentClient.clientSecret());
clientRepository.update(updated);
return toClient(updated);
}
private static String toScopeString(final ClientData clientData) {
return String.join(" ", clientData.scopes());
}
private static String getRedirectUri(final ClientData clientData) {
return clientData.redirectURI() != null ? clientData.redirectURI().toString() : null;
}
@Override
public Optional<Client> getClient(final Principal principal, final String id) {
boolean ownsClient = clientRepository.getOwners(id).contains(principal.getName());

@ -7,6 +7,8 @@ import java.util.Optional;
public interface ClientRepository {
void addNewClient(ClientRow clientRow);
void update(ClientRow clientRow);
List<ClientRow> getClients(Principal owner);
void addClientOwner(String principalName, String id);

@ -116,6 +116,43 @@ public class ClientAdminController {
}
@GetMapping("/{id}/edit")
public String showEditClient(
@PathVariable("id") String id,
Principal principal,
Model model)
{
Client client = clientManagementService.getClient(principal, id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
model.addAttribute("client", client);
model.addAttribute("newClient", NewClientRequest.from(client));
return "admin/client/edit";
}
@PostMapping("/{id}/edit")
public String editClient(
@PathVariable("id") String id,
Principal principal,
Model model,
RedirectAttributes redirectAttributes,
@ModelAttribute("newClient") NewClientRequest newClientRequest,
BindingResult bindingResult)
{
if (bindingResult.hasErrors()) {
model.addAttribute("errors", bindingResult.getAllErrors());
return "admin/client/edit";
}
ClientData clientData = new ClientData(
newClientRequest.name(),
newClientRequest.redirectUri(),
newClientRequest.isPublic(),
newClientRequest.scopes(),
newClientRequest.contact());
final Client updatedClient = clientManagementService.updateClient(principal, id, clientData);
redirectAttributes.addFlashAttribute("message", "Client updated");
return "redirect:/admin/client/" + updatedClient.id();
}
@ModelAttribute
public CsrfToken csrfToken(CsrfToken token) {
return token;

@ -1,5 +1,7 @@
package se.su.dsv.oauth2.web.client;
import se.su.dsv.oauth2.admin.Client;
import java.net.URI;
import java.util.Arrays;
import java.util.Objects;
@ -14,6 +16,15 @@ public record NewClientRequest(
String public_,
String scope)
{
public static NewClientRequest from(final Client client) {
return new NewClientRequest(
client.name(),
client.contact(),
URI.create(client.redirectUri()),
client.isPublic() ? "on" : null,
String.join("\r\n", client.scopes()));
}
public boolean isPublic() {
return "on".equals(public_);
}

@ -0,0 +1,78 @@
@import java.util.List
@import org.springframework.validation.ObjectError
@import se.su.dsv.oauth2.admin.Client
@param org.springframework.security.web.csrf.CsrfToken csrfToken
@param se.su.dsv.oauth2.web.client.NewClientRequest newClient
@param Client client
@param String feedback
@param List<ObjectError> errors
@template.base(title = "Edit client " + client.name(), content = @`
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="/">Home</a>
</li>
<li class="breadcrumb-item">
<a href="/admin">Administration</a>
</li>
<li class="breadcrumb-item">
<a href="/admin/client">Registered clients</a>
</li>
<li class="breadcrumb-item active" aria-current="page">
Edit client ${client.name()}
</li>
</ol>
</nav>
<h1>Edit client ${client.name()}</h1>
<form action="/admin/client/${client.id()}/edit" method="POST">
@if (errors != null)
@for (ObjectError error : errors)
<div class="alert alert-danger" role="alert">
${error.getDefaultMessage()}
</div>
@endfor
@endif
<input type="hidden" name="${csrfToken.getParameterName()}" value="${csrfToken.getToken()}">
<div class="mb-3">
<label class="form-label" for="name">Name</label>
<input class="form-control" name="name" id="name" required value="${newClient.name()}">
</div>
<div class="mb-3">
<label class="form-label" for="contact">Contact e-mail address</label>
<input class="form-control" name="contact" id="contact" type="email" required value="${newClient.contact()}">
<small class="text-muted">A place where we can contact you should the need arise.</small>
</div>
<div class="mb-3">
<label class="form-label" for="redirectUri">Redirect URI</label>
<input class="form-control" name="redirectUri" id="redirectUri" type="url" value="${newClient.redirectUriString()}">
</div>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" id="public" name="public_" checked="${newClient.isPublic()}">
<label class="form-check-label" for="public">Public client</label>
<br>
<small class="text-muted">
A public client has no secret and requires
<a href="https://datatracker.ietf.org/doc/html/rfc7636/">PKCE</a>
for authorization code flow.
It can not be issued
<a href="https://datatracker.ietf.org/doc/html/rfc6749#section-1.5">refresh tokens</a>.
</small>
</div>
<div class="mb-3">
<label class="form-label" for="scope">Scopes</label>
<textarea class="form-control" name="scope" id="scope" rows="5">${newClient.scope()}</textarea>
<small class="text-muted">
The set of scopes this client is allowed to request, one per line.
Common scopes include
<a href="https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims">Open ID Connect scopes</a>.
Refer to the resource server documentation for more information.
</small>
</div>
<button type="submit" class="btn btn-primary">Update client</button>
<a href="/admin/client/${client.id()}" class="btn btn-link">Cancel</a>
</form>
`)

@ -52,6 +52,10 @@
</div>
@endif
<aside class="float-end">
<a href="/admin/client/${client.id()}/edit" class="btn btn-link">Edit</a>
</aside>
<dl>
<dt>Name</dt>
<dd>${client.name()}</dd>