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

View file

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

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

View file

@ -35,7 +35,7 @@
<body>
<!-- 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}"
catalog-elements-limit="${CATALOG_ELEMENTS_LIMIT}" single-registry="${SINGLE_REGISTRY}">
</docker-registry-ui>