parent
a4f99f1b29
commit
c9559ca930
src/main
java/se/su/dsv/oauth2
resources/templates/admin/client
@ -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_);
|
||||
}
|
||||
|
78
src/main/resources/templates/admin/client/edit.jte
Normal file
78
src/main/resources/templates/admin/client/edit.jte
Normal file
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user