feat(token-auth-keycloak): remove custom dialog and use browser basic auth with nginx configuration

This commit is contained in:
Joxit 2021-04-09 17:28:48 +02:00
parent 58b1486c81
commit bc5082dcf9
No known key found for this signature in database
GPG key ID: F526592B8E012263
7 changed files with 53 additions and 98 deletions

View file

@ -34,6 +34,17 @@ server {
proxy_pass http://keycloak:8080; proxy_pass http://keycloak:8080;
} }
location /auth/realms/master/protocol/docker-v2/auth {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
if ($http_authorization = "") {
add_header WWW-Authenticate 'Basic realm="Keycloak login"' always;
return 401;
}
proxy_pass http://keycloak:8080;
}
location /ui { location /ui {
proxy_pass http://ui/; proxy_pass http://ui/;
} }

View file

@ -1,71 +0,0 @@
<!--
Copyright (C) 2016-2021 Jones Magloire @Joxit
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<registry-authentication>
<material-popup opened="{ props.opened }" onClick="{ props.onClose }">
<div slot="title">Sign In to your registry</div>
<div slot="content">
<material-input placeholder="Username" id="username"></material-input>
<material-input placeholder="Password" id="password" onkeyup="{ onKeyUp }" type="password"></material-input>
</div>
<div slot="action">
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ signIn }">
Sign In
</material-button>
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ props.onClose }">
Cancel
</material-button>
</div>
</material-popup>
<script>
export default {
signIn() {
const {
realm,
service,
scope,
onAuthenticated,
onClose
} = this.props;
const username = this.$('#username input').value;
const password = this.$('#password input').value;
const req = new XMLHttpRequest()
req.addEventListener('loadend', () => {
try {
const bearer = JSON.parse(req.responseText);
onAuthenticated(bearer)
localStorage.setItem('registry.token', bearer.token);
localStorage.setItem('registry.issued_at', bearer.issued_at);
localStorage.setItem('registry.expires_in', bearer.expires_in);
onClose();
} catch (e) {
console.log(e);
}
})
req.open('GET', `${realm}?service=${service}&scope=${scope}`)
req.setRequestHeader('Authorization', `Basic ${btoa(`${username}:${password}`)}`)
req.send()
},
onKeyUp(event) {
// if keyCode is Enter
if (event.keyCode === 13) {
this.signIn();
}
},
}
</script>
</registry-authentication>

View file

@ -39,7 +39,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<route path="{baseRoute}taghistory/(.*)"> <route path="{baseRoute}taghistory/(.*)">
<tag-history registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }" <tag-history registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
image="{ router.getTagHistoryImage() }" tag="{ router.getTagHistoryTag() }" image="{ router.getTagHistoryImage() }" tag="{ router.getTagHistoryTag() }"
is-image-remove-activated="{ props.isImageRemoveActivated }" on-notify="{ notifySnackbar }" on-authentication="{ onAuthentication }"></tag-history> is-image-remove-activated="{ props.isImageRemoveActivated }" on-notify="{ notifySnackbar }"
on-authentication="{ onAuthentication }"></tag-history>
</route> </route>
</router> </router>
<registry-authentication realm="{ state.realm }" scope="{ state.scope }" service="{ state.service }" <registry-authentication realm="{ state.realm }" scope="{ state.scope }" service="{ state.service }"
@ -73,7 +74,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import TagHistory from './tag-history/tag-history.riot'; import TagHistory from './tag-history/tag-history.riot';
import DialogsMenu from './dialogs/dialogs-menu.riot'; import DialogsMenu from './dialogs/dialogs-menu.riot';
import SearchBar from './search-bar.riot' import SearchBar from './search-bar.riot'
import RegistryAuthentication from './dialogs/registry-authentication.riot';
import { import {
stripHttps, stripHttps,
getRegistryServers getRegistryServers
@ -87,7 +87,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
TagHistory, TagHistory,
DialogsMenu, DialogsMenu,
SearchBar, SearchBar,
RegistryAuthentication,
Router, Router,
Route Route
}, },
@ -113,20 +112,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}) })
}, },
onAuthentication(opts, onAuthenticated) { onAuthentication(opts, onAuthenticated) {
const bearer = { if (opts && opts.realm && opts.service && opts.scope) {
token: localStorage.getItem('registry.token'), const {
issued_at: localStorage.getItem('registry.issued_at'), realm,
expires_in: localStorage.getItem('registry.expires_in') service,
} scope,
if (bearer.token && bearer.issued_at && bearer.expires_in && } = opts;
(new Date().getTime() - new Date(bearer.issued_at).getTime()) < (bearer.expires_in * 1000)) { const req = new XMLHttpRequest()
onAuthenticated(bearer) req.addEventListener('loadend', () => {
} else if (opts) { try {
this.update({ const bearer = JSON.parse(req.responseText);
authenticationDialogOpened: true, onAuthenticated(bearer)
onAuthenticated, } catch (e) {
...opts this.notifySnackbar(`Failed to log in: ${e.message}`, true)
}
}) })
req.open('GET', `${realm}?service=${service}&scope=${scope}`)
req.send()
} else { } else {
onAuthenticated() onAuthenticated()
} }

View file

@ -43,21 +43,32 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
state.checked = props.checked; state.checked = props.checked;
}, },
deleteImage(ignoreError) { deleteImage(ignoreError) {
deleteImage(this.props.image, this.props.registryUrl, ignoreError, this.props.onNotify) deleteImage(this.props.image, {
...this.props,
ignoreError
})
}, },
handleCheckboxChange(checked) { handleCheckboxChange(checked) {
this.props.handleCheckboxChange(checked, this.props.image); this.props.handleCheckboxChange(checked, this.props.image);
} }
} }
export function deleteImage(image, registryUrl, ignoreError, onNotify) { export function deleteImage(image, opts) {
const {
registryUrl,
ignoreError,
onNotify,
onAuthentication
} = opts;
if (!image.digest) { if (!image.digest) {
onNotify(`Information for ${name}:${tag} are not yet loaded.`); onNotify(`Information for ${name}:${tag} are not yet loaded.`);
return; return;
} }
const name = image.name; const name = image.name;
const tag = image.tag; const tag = image.tag;
const oReq = new Http(); const oReq = new Http({
onAuthentication: onAuthentication
});
oReq.addEventListener('loadend', function () { oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) { if (this.status == 200 || this.status == 202) {
router.taglist(name); router.taglist(name);
@ -71,7 +82,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
onNotify(this.responseText); onNotify(this.responseText);
} }
}); });
oReq.open('DELETE', registryUrl + '/v2/' + name + '/manifests/' + image.digest); oReq.open('DELETE', `${registryUrl}/v2/${name}/manifests/${image.digest}`);
oReq.setRequestHeader('Accept', oReq.setRequestHeader('Accept',
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json'); 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json');
oReq.addEventListener('error', function () { oReq.addEventListener('error', function () {

View file

@ -39,7 +39,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<tag-table if="{ state.loadend }" tags="{state.tags}" asc="{state.asc}" page="{ state.page }" <tag-table if="{ state.loadend }" tags="{state.tags}" asc="{state.asc}" page="{ state.page }"
show-content-digest="{props.showContentDigest}" is-image-remove-activated="{props.isImageRemoveActivated}" show-content-digest="{props.showContentDigest}" is-image-remove-activated="{props.isImageRemoveActivated}"
onReverseOrder="{ onReverseOrder }" registry-url="{ props.registryUrl }" pull-url="{ props.pullUrl }" onReverseOrder="{ onReverseOrder }" registry-url="{ props.registryUrl }" pull-url="{ props.pullUrl }"
on-notify="{ props.onNotify }" filter-results="{ props.filterResults }"></tag-table> on-notify="{ props.onNotify }" filter-results="{ props.filterResults }"
on-authentication="{ props.onAuthentication }">
</tag-table>
<pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination> <pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination>

View file

@ -63,16 +63,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<td if="{ props.isImageRemoveActivated }" class="remove-tag"> <td if="{ props.isImageRemoveActivated }" class="remove-tag">
<remove-image multi-delete="{ state.multiDelete }" image="{ image }" registry-url="{ props.registryUrl }" <remove-image multi-delete="{ state.multiDelete }" image="{ image }" registry-url="{ props.registryUrl }"
handleCheckboxChange="{ onRemoveImageChange }" checked="{ state.toDelete.has(image) }" handleCheckboxChange="{ onRemoveImageChange }" checked="{ state.toDelete.has(image) }"
on-notify="{ props.onNotify }" /> on-notify="{ props.onNotify }" on-authentication="{ props.onAuthentication }" />
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</material-card> </material-card>
<script> <script>
import {
Http
} from '../../scripts/http';
import { import {
getPage, getPage,
} from '../../scripts/utils'; } from '../../scripts/utils';
@ -112,7 +109,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
state.page = props.page state.page = props.page
}, },
bulkDelete() { bulkDelete() {
this.state.toDelete.forEach(image => deleteImage(image, this.props.registryUrl, true, this.props.onNotify)) this.state.toDelete.forEach(image => deleteImage(image, {
...this.props,
ignoreError: true
}))
}, },
onRemoveImageHeaderChange(checked, event) { onRemoveImageHeaderChange(checked, event) {
if (event.altKey === true) { if (event.altKey === true) {

View file

@ -35,7 +35,7 @@
<body> <body>
<!-- build:keep production --> <!-- build:keep production -->
<docker-registry-ui registry-url="${URL}" name="${REGISTRY_TITLE}" pull-url="${PULL_URL}" <docker-registry-ui registry-url="${REGISTRY_URL}" name="${REGISTRY_TITLE}" pull-url="${PULL_URL}"
show-content-digest="${SHOW_CONTENT_DIGEST}" is-image-remove-activated="${DELETE_IMAGES}" show-content-digest="${SHOW_CONTENT_DIGEST}" is-image-remove-activated="${DELETE_IMAGES}"
catalog-elements-limit="${CATALOG_ELEMENTS_LIMIT}" single-registry="${SINGLE_REGISTRY}"> catalog-elements-limit="${CATALOG_ELEMENTS_LIMIT}" single-registry="${SINGLE_REGISTRY}">
</docker-registry-ui> </docker-registry-ui>