mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-02-01 04:09:08 +01:00
add config form for public keys
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import sonia.scm.BadRequestException;
|
||||
import sonia.scm.ContextEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class NotPublicKeyException extends BadRequestException {
|
||||
public NotPublicKeyException(List<ContextEntry> context, String message) {
|
||||
super(context, message);
|
||||
}
|
||||
|
||||
public NotPublicKeyException(List<ContextEntry> context, String message, Exception cause) {
|
||||
super(context, message, cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "BxS5wX2v71";
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ import java.security.Principal;
|
||||
@StaticPermissions(
|
||||
value = "user",
|
||||
globalPermissions = {"create", "list", "autocomplete"},
|
||||
permissions = {"read", "modify", "delete", "changePassword"},
|
||||
permissions = {"read", "modify", "delete", "changePassword", "changePublicKeys"},
|
||||
custom = true, customGlobal = true
|
||||
)
|
||||
@XmlRootElement(name = "users")
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
"settingsNavLink": "Einstellungen",
|
||||
"generalNavLink": "Generell",
|
||||
"setPasswordNavLink": "Passwort",
|
||||
"setPermissionsNavLink": "Berechtigungen"
|
||||
"setPermissionsNavLink": "Berechtigungen",
|
||||
"setPublicKeyNavLink": "Öffentliche Schlüssel"
|
||||
}
|
||||
},
|
||||
"createUser": {
|
||||
@@ -60,5 +61,13 @@
|
||||
"userForm": {
|
||||
"subtitle": "Benutzer bearbeiten",
|
||||
"button": "Speichern"
|
||||
},
|
||||
"publicKey": {
|
||||
"noStoredKeys": "Es wurden keine Schlüssel gefunden.",
|
||||
"displayName": "Anzeigename",
|
||||
"raw": "Schlüssel",
|
||||
"created": "Eingetragen an",
|
||||
"addKey": "Schlüssel hinzufügen",
|
||||
"delete": "Löschen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
"settingsNavLink": "Settings",
|
||||
"generalNavLink": "General",
|
||||
"setPasswordNavLink": "Password",
|
||||
"setPermissionsNavLink": "Permissions"
|
||||
"setPermissionsNavLink": "Permissions",
|
||||
"setPublicKeyNavLink": "Public Keys"
|
||||
}
|
||||
},
|
||||
"createUser": {
|
||||
@@ -60,5 +61,13 @@
|
||||
"userForm": {
|
||||
"subtitle": "Edit User",
|
||||
"button": "Submit"
|
||||
},
|
||||
"publicKey": {
|
||||
"noStoredKeys": "No keys found.",
|
||||
"displayName": "Display Name",
|
||||
"raw": "Key",
|
||||
"created": "Created on",
|
||||
"addKey": "Add key",
|
||||
"delete": "Delete"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ import {
|
||||
import ChangeUserPassword from "./ChangeUserPassword";
|
||||
import ProfileInfo from "./ProfileInfo";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import SetPublicKeys from "../users/components/publicKeys/SetPublicKeys";
|
||||
import SetPublicKeyNavLink from "../users/components/navLinks/SetPublicKeysNavLink";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
@@ -93,6 +95,7 @@ class Profile extends React.Component<Props> {
|
||||
<PrimaryContentColumn>
|
||||
<Route path={url} exact render={() => <ProfileInfo me={me} />} />
|
||||
<Route path={`${url}/settings/password`} render={() => <ChangeUserPassword me={me} />} />
|
||||
<Route path={`${url}/settings/publicKeys`} render={() => <SetPublicKeys user={me}/>} />
|
||||
<ExtensionPoint name="profile.route" props={extensionProps} renderAll={true} />
|
||||
</PrimaryContentColumn>
|
||||
<SecondaryNavigationColumn>
|
||||
@@ -109,6 +112,7 @@ class Profile extends React.Component<Props> {
|
||||
title={t("profile.settingsNavLink")}
|
||||
>
|
||||
<NavLink to={`${url}/settings/password`} label={t("profile.changePasswordNavLink")} />
|
||||
<SetPublicKeyNavLink user={me} publicKeyUrl={`${url}/settings/publicKeys`} />
|
||||
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
|
||||
</SubNavigation>
|
||||
</SecondaryNavigation>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
import { Link, User, Me } from "@scm-manager/ui-types";
|
||||
import { NavLink } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
user: User | Me;
|
||||
publicKeyUrl: string;
|
||||
};
|
||||
|
||||
const SetPublicKeyNavLink: FC<Props> = ({ user, publicKeyUrl }) => {
|
||||
const [t] = useTranslation("users");
|
||||
|
||||
if ((user?._links?.publicKeys as Link)?.href) {
|
||||
return <NavLink to={publicKeyUrl} label={t("singleUser.menu.setPublicKeyNavLink")} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default SetPublicKeyNavLink;
|
||||
@@ -25,3 +25,4 @@
|
||||
export { default as EditUserNavLink } from "./EditUserNavLink";
|
||||
export { default as SetPasswordNavLink } from "./SetPasswordNavLink";
|
||||
export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink";
|
||||
export { default as SetPublicKeysNavLink } from "./SetPublicKeysNavLink";
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { FC, useState } from "react";
|
||||
import { User, Link, Links, Collection } from "@scm-manager/ui-types/src";
|
||||
import {
|
||||
ErrorNotification,
|
||||
InputField,
|
||||
Level,
|
||||
Textarea,
|
||||
SubmitButton,
|
||||
apiClient,
|
||||
Loading
|
||||
} from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CONTENT_TYPE_PUBLIC_KEY } from "./SetPublicKeys";
|
||||
|
||||
type Props = {
|
||||
createLink: string;
|
||||
refresh: () => void;
|
||||
};
|
||||
|
||||
const AddPublicKey: FC<Props> = ({ createLink, refresh }) => {
|
||||
const [t] = useTranslation("users");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<undefined | Error>();
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [raw, setRaw] = useState("");
|
||||
|
||||
const isValid = () => {
|
||||
return !!displayName && !!raw;
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setDisplayName("");
|
||||
setRaw("");
|
||||
};
|
||||
|
||||
const addKey = () => {
|
||||
setLoading(true);
|
||||
apiClient
|
||||
.post(createLink, { displayName: displayName, raw: raw }, CONTENT_TYPE_PUBLIC_KEY)
|
||||
.then(resetForm)
|
||||
.then(refresh)
|
||||
.then(() => setLoading(false))
|
||||
.catch(setError);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<InputField label={t("publicKey.displayName")} value={displayName} onChange={setDisplayName} />
|
||||
<Textarea name="raw" label={t("publicKey.raw")} value={raw} onChange={setRaw} />
|
||||
<Level
|
||||
right={<SubmitButton label={t("publicKey.addKey")} loading={loading} disabled={!isValid()} action={addKey} />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPublicKey;
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { DateFromNow, DeleteButton } from "@scm-manager/ui-components/src";
|
||||
import { PublicKey } from "./SetPublicKeys";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "@scm-manager/ui-types";
|
||||
import { formatPublicKey } from "./formatPublicKey";
|
||||
|
||||
type Props = {
|
||||
publicKey: PublicKey;
|
||||
onDelete: (link: string) => void;
|
||||
};
|
||||
|
||||
export const PublicKeyEntry: FC<Props> = ({ publicKey, onDelete }) => {
|
||||
const [t] = useTranslation("users");
|
||||
|
||||
let deleteButton;
|
||||
if (publicKey?._links?.delete) {
|
||||
deleteButton = (
|
||||
<DeleteButton label={t("publicKey.delete")} action={() => onDelete((publicKey._links.delete as Link).href)} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td>{publicKey.displayName}</td>
|
||||
<td>
|
||||
<DateFromNow date={publicKey.created} />
|
||||
</td>
|
||||
<td className="is-hidden-mobile">{formatPublicKey(publicKey.raw)}</td>
|
||||
<td>{deleteButton}</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicKeyEntry;
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PublicKey, PublicKeysCollection } from "./SetPublicKeys";
|
||||
import PublicKeyEntry from "./PublicKeyEntry";
|
||||
import { Notification } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
publicKeys?: PublicKeysCollection;
|
||||
onDelete: (link: string) => void;
|
||||
};
|
||||
|
||||
const PublicKeyTable: FC<Props> = ({ publicKeys, onDelete }) => {
|
||||
const [t] = useTranslation("users");
|
||||
|
||||
if (publicKeys?._embedded?.keys?.length === 0) {
|
||||
return <Notification type="info">{t("publicKey.noStoredKeys")}</Notification>;
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="card-table table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("publicKey.displayName")}</th>
|
||||
<th>{t("publicKey.created")}</th>
|
||||
<th className="is-hidden-mobile">{t("publicKey.raw")}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{publicKeys?._embedded?.keys?.map((publicKey: PublicKey, index: number) => {
|
||||
return <PublicKeyEntry key={index} onDelete={onDelete} publicKey={publicKey} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicKeyTable;
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Collection, Link, Links, User, Me } from "@scm-manager/ui-types";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import AddPublicKey from "./AddPublicKey";
|
||||
import PublicKeyTable from "./PublicKeyTable";
|
||||
import { apiClient, ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||
|
||||
export type PublicKeysCollection = Collection & {
|
||||
_embedded: {
|
||||
keys: PublicKey[];
|
||||
};
|
||||
};
|
||||
|
||||
export type PublicKey = {
|
||||
displayName: string;
|
||||
raw: string;
|
||||
created?: string;
|
||||
_links: Links;
|
||||
};
|
||||
|
||||
export const CONTENT_TYPE_PUBLIC_KEY = "application/vnd.scmm-publicKey+json;v=2";
|
||||
|
||||
type Props = {
|
||||
user: User | Me;
|
||||
};
|
||||
|
||||
const SetPublicKeys: FC<Props> = ({ user }) => {
|
||||
const [error, setError] = useState<undefined | Error>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [publicKeys, setPublicKeys] = useState<PublicKeysCollection | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPublicKeys();
|
||||
}, [user]);
|
||||
|
||||
const fetchPublicKeys = () => {
|
||||
setLoading(true);
|
||||
apiClient
|
||||
.get((user._links.publicKeys as Link).href)
|
||||
.then(r => r.json())
|
||||
.then(setPublicKeys)
|
||||
.then(() => setLoading(false))
|
||||
.catch(setError);
|
||||
};
|
||||
|
||||
const onDelete = (link: string) => {
|
||||
apiClient
|
||||
.delete(link)
|
||||
.then(fetchPublicKeys)
|
||||
.catch(setError);
|
||||
};
|
||||
|
||||
const createLink = (publicKeys?._links?.create as Link)?.href;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PublicKeyTable publicKeys={publicKeys} onDelete={onDelete} />
|
||||
{createLink && <AddPublicKey createLink={createLink} refresh={fetchPublicKeys} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetPublicKeys;
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { formatPublicKey } from "./formatPublicKey";
|
||||
|
||||
describe("format authorized key tests", () => {
|
||||
it("should format the given key", () => {
|
||||
const key = "ssh-rsa ACB0DEFGHIJKLMOPQRSTUVWXYZ tricia@hitchhiker.com";
|
||||
expect(formatPublicKey(key)).toEqual("ssh-rsa ... tricia@hitchhiker.com");
|
||||
});
|
||||
|
||||
it("should use the first chars of the key without prefix", () => {
|
||||
const key = "ACB0DEFGHIJKLMOPQRSTUVWXYZ tricia@hitchhiker.com";
|
||||
expect(formatPublicKey(key)).toEqual("ACB0DEF... tricia@hitchhiker.com");
|
||||
});
|
||||
|
||||
it("should use the last chars of the key without suffix", () => {
|
||||
const key = "ssh-rsa ACB0DEFGHIJKLMOPQRSTUVWXYZ";
|
||||
expect(formatPublicKey(key)).toEqual("ssh-rsa ...TUVWXYZ");
|
||||
});
|
||||
|
||||
it("should use a few chars from the beginning and a few from the end, if the key has no prefix and suffix", () => {
|
||||
const key = "ACB0DEFGHIJKLMOPQRSTUVWXYZ0123456789";
|
||||
expect(formatPublicKey(key)).toEqual("ACB0DEF...3456789");
|
||||
});
|
||||
|
||||
it("should return the whole string for a short key", () => {
|
||||
const key = "ABCDE";
|
||||
expect(formatPublicKey(key)).toEqual("ABCDE");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
export const formatPublicKey = (key: string) => {
|
||||
const parts = key.split(/\s+/);
|
||||
if (parts.length === 3) {
|
||||
return parts[0] + " ... " + parts[2];
|
||||
} else if (parts.length === 2) {
|
||||
if (parts[0].length >= parts[1].length) {
|
||||
return parts[0].substring(0, 7) + "... " + parts[1];
|
||||
} else {
|
||||
const keyLength = parts[1].length;
|
||||
return parts[0] + " ..." + parts[1].substring(keyLength - 7);
|
||||
}
|
||||
} else {
|
||||
const keyLength = parts[0].length;
|
||||
if (keyLength < 15) {
|
||||
return parts[0];
|
||||
}
|
||||
return parts[0].substring(0, 7) + "..." + parts[0].substring(keyLength - 7);
|
||||
}
|
||||
};
|
||||
@@ -41,11 +41,13 @@ import {
|
||||
import { Details } from "./../components/table";
|
||||
import EditUser from "./EditUser";
|
||||
import { fetchUserByName, getFetchUserFailure, getUserByName, isFetchUserPending } from "../modules/users";
|
||||
import { EditUserNavLink, SetPasswordNavLink, SetPermissionsNavLink } from "./../components/navLinks";
|
||||
import { EditUserNavLink, SetPasswordNavLink, SetPermissionsNavLink, SetPublicKeysNavLink } from "./../components/navLinks";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { getUsersLink } from "../../modules/indexResource";
|
||||
import SetUserPassword from "../components/SetUserPassword";
|
||||
import SetPermissions from "../../permissions/components/SetPermissions";
|
||||
import AddPublicKey from "../components/publicKeys/AddPublicKey";
|
||||
import SetPublicKeys from "../components/publicKeys/SetPublicKeys";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
@@ -105,6 +107,10 @@ class SingleUser extends React.Component<Props> {
|
||||
path={`${url}/settings/permissions`}
|
||||
component={() => <SetPermissions selectedPermissionsLink={user._links.permissions} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/settings/publickeys`}
|
||||
component={() => <SetPublicKeys user={user} />}
|
||||
/>
|
||||
<ExtensionPoint name="user.route" props={extensionProps} renderAll={true} />
|
||||
</PrimaryContentColumn>
|
||||
<SecondaryNavigationColumn>
|
||||
@@ -123,6 +129,7 @@ class SingleUser extends React.Component<Props> {
|
||||
<EditUserNavLink user={user} editUrl={`${url}/settings/general`} />
|
||||
<SetPasswordNavLink user={user} passwordUrl={`${url}/settings/password`} />
|
||||
<SetPermissionsNavLink user={user} permissionsUrl={`${url}/settings/permissions`} />
|
||||
<SetPublicKeysNavLink user={user} publicKeyUrl={`${url}/settings/publickeys`} />
|
||||
<ExtensionPoint name="user.setting" props={extensionProps} renderAll={true} />
|
||||
</SubNavigation>
|
||||
</SecondaryNavigation>
|
||||
|
||||
@@ -27,6 +27,7 @@ package sonia.scm.api.v2.resources;
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.servlet.ServletScopes;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
import sonia.scm.security.gpg.PublicKeyMapper;
|
||||
import sonia.scm.web.api.RepositoryToHalMapper;
|
||||
|
||||
public class MapperModule extends AbstractModule {
|
||||
@@ -35,6 +36,7 @@ public class MapperModule extends AbstractModule {
|
||||
bind(UserDtoToUserMapper.class).to(Mappers.getMapperClass(UserDtoToUserMapper.class));
|
||||
bind(UserToUserDtoMapper.class).to(Mappers.getMapperClass(UserToUserDtoMapper.class));
|
||||
bind(UserCollectionToDtoMapper.class);
|
||||
bind(PublicKeyMapper.class).to(Mappers.getMapperClass(PublicKeyMapper.class));
|
||||
|
||||
bind(GroupDtoToGroupMapper.class).to(Mappers.getMapperClass(GroupDtoToGroupMapper.class));
|
||||
bind(GroupToGroupDtoMapper.class).to(Mappers.getMapperClass(GroupToGroupDtoMapper.class));
|
||||
|
||||
@@ -89,6 +89,9 @@ public class MeDtoFactory extends HalAppenderMapper {
|
||||
if (UserPermissions.modify(user).isPermitted()) {
|
||||
linksBuilder.single(link("update", resourceLinks.me().update(user.getName())));
|
||||
}
|
||||
if (UserPermissions.changePublicKeys(user).isPermitted()) {
|
||||
linksBuilder.single(link("publicKeys", resourceLinks.user().publicKeys(user.getName())));
|
||||
}
|
||||
if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted() && !Authentications.isSubjectAnonymous(user.getName())) {
|
||||
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.security.gpg.PublicKeyResource;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.net.URI;
|
||||
@@ -99,9 +100,11 @@ class ResourceLinks {
|
||||
|
||||
static class UserLinks {
|
||||
private final LinkBuilder userLinkBuilder;
|
||||
private final LinkBuilder publicKeyLinkBuilder;
|
||||
|
||||
UserLinks(ScmPathInfo pathInfo) {
|
||||
userLinkBuilder = new LinkBuilder(pathInfo, UserRootResource.class, UserResource.class);
|
||||
publicKeyLinkBuilder = new LinkBuilder(pathInfo, PublicKeyResource.class);
|
||||
}
|
||||
|
||||
String self(String name) {
|
||||
@@ -119,6 +122,10 @@ class ResourceLinks {
|
||||
public String passwordChange(String name) {
|
||||
return userLinkBuilder.method("getUserResource").parameters(name).method("overwritePassword").parameters().href();
|
||||
}
|
||||
|
||||
public String publicKeys(String name) {
|
||||
return publicKeyLinkBuilder.method("findAll").parameters(name).href();
|
||||
}
|
||||
}
|
||||
|
||||
interface WithPermissionLinks {
|
||||
|
||||
@@ -65,6 +65,7 @@ public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
|
||||
}
|
||||
if (UserPermissions.modify(user).isPermitted()) {
|
||||
linksBuilder.single(link("update", resourceLinks.user().update(user.getName())));
|
||||
linksBuilder.single(link("publicKeys", resourceLinks.user().publicKeys(user.getName())));
|
||||
if (userManager.isTypeDefault(user)) {
|
||||
linksBuilder.single(link("password", resourceLinks.user().passwordChange(user.getName())));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Link;
|
||||
import de.otto.edison.hal.Links;
|
||||
import sonia.scm.api.v2.resources.LinkBuilder;
|
||||
import sonia.scm.api.v2.resources.ScmPathInfoStore;
|
||||
import sonia.scm.user.UserPermissions;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
|
||||
public class PublicKeyCollectionMapper {
|
||||
|
||||
private final Provider<ScmPathInfoStore> scmPathInfoStore;
|
||||
private final PublicKeyMapper mapper;
|
||||
|
||||
@Inject
|
||||
public PublicKeyCollectionMapper(Provider<ScmPathInfoStore> scmPathInfoStore, PublicKeyMapper mapper) {
|
||||
this.scmPathInfoStore = scmPathInfoStore;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
HalRepresentation map(String username, List<RawGpgKey> keys) {
|
||||
List<RawGpgKeyDto> dtos = keys.stream()
|
||||
.map(mapper::map)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Links.Builder builder = linkingTo();
|
||||
|
||||
builder.self(selfLink(username));
|
||||
|
||||
if (hasCreatePermissions(username)) {
|
||||
builder.single(Link.link("create", createLink(username)));
|
||||
}
|
||||
|
||||
return new HalRepresentation(builder.build(), Embedded.embedded("keys", dtos));
|
||||
}
|
||||
|
||||
private boolean hasCreatePermissions(String username) {
|
||||
return UserPermissions.changePublicKeys(username).isPermitted();
|
||||
}
|
||||
|
||||
private String createLink(String username) {
|
||||
return new LinkBuilder(scmPathInfoStore.get().get(), PublicKeyResource.class)
|
||||
.method("create")
|
||||
.parameters(username)
|
||||
.href();
|
||||
}
|
||||
|
||||
private String selfLink(String username) {
|
||||
return new LinkBuilder(scmPathInfoStore.get().get(), PublicKeyResource.class)
|
||||
.method("findAll")
|
||||
.parameters(username)
|
||||
.href();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import de.otto.edison.hal.Link;
|
||||
import de.otto.edison.hal.Links;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.ObjectFactory;
|
||||
import sonia.scm.api.v2.resources.LinkBuilder;
|
||||
import sonia.scm.api.v2.resources.ScmPathInfoStore;
|
||||
import sonia.scm.user.UserPermissions;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
|
||||
@Mapper
|
||||
public abstract class PublicKeyMapper {
|
||||
|
||||
@Inject
|
||||
private Provider<ScmPathInfoStore> scmPathInfoStore;
|
||||
|
||||
@VisibleForTesting
|
||||
void setScmPathInfoStore(Provider<ScmPathInfoStore> scmPathInfoStore) {
|
||||
this.scmPathInfoStore = scmPathInfoStore;
|
||||
}
|
||||
|
||||
@Mapping(target = "attributes", ignore = true)
|
||||
abstract RawGpgKeyDto map(RawGpgKey rawGpgKey);
|
||||
|
||||
@ObjectFactory
|
||||
RawGpgKeyDto createDto(RawGpgKey rawGpgKey) {
|
||||
Links.Builder linksBuilder = linkingTo();
|
||||
linksBuilder.self(createSelfLink(rawGpgKey));
|
||||
if (UserPermissions.changePublicKeys(rawGpgKey.getOwner()).isPermitted()) {
|
||||
linksBuilder.single(Link.link("delete", createDeleteLink(rawGpgKey)));
|
||||
}
|
||||
return new RawGpgKeyDto(linksBuilder.build());
|
||||
}
|
||||
|
||||
private String createSelfLink(RawGpgKey rawGpgKey) {
|
||||
return new LinkBuilder(scmPathInfoStore.get().get(), PublicKeyResource.class)
|
||||
.method("findById")
|
||||
.parameters(rawGpgKey.getId())
|
||||
.href();
|
||||
}
|
||||
|
||||
private String createDeleteLink(RawGpgKey rawGpgKey) {
|
||||
return new LinkBuilder(scmPathInfoStore.get().get(), PublicKeyResource.class)
|
||||
.method("deleteById")
|
||||
.parameters(rawGpgKey.getId())
|
||||
.href();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import sonia.scm.api.v2.resources.ErrorDto;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.util.Optional;
|
||||
|
||||
@Path("v2/public_keys")
|
||||
public class PublicKeyResource {
|
||||
|
||||
private static final String MEDIA_TYPE = VndMediaType.PREFIX + "publicKey" + VndMediaType.SUFFIX;
|
||||
private static final String MEDIA_TYPE_COLLECTION = VndMediaType.PREFIX + "publicKeyCollection" + VndMediaType.SUFFIX;
|
||||
|
||||
private final PublicKeyMapper mapper;
|
||||
private final PublicKeyCollectionMapper collectionMapper;
|
||||
private final PublicKeyStore store;
|
||||
|
||||
@Inject
|
||||
public PublicKeyResource(PublicKeyMapper mapper, PublicKeyCollectionMapper collectionMapper, PublicKeyStore store) {
|
||||
this.mapper = mapper;
|
||||
this.collectionMapper = collectionMapper;
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}")
|
||||
@Produces(MEDIA_TYPE_COLLECTION)
|
||||
@Operation(
|
||||
summary = "Get all public keys for user",
|
||||
description = "Returns all keys for the given username.",
|
||||
tags = "User",
|
||||
operationId = "get_all_public_keys"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = MEDIA_TYPE_COLLECTION,
|
||||
schema = @Schema(implementation = HalRepresentation.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized / the current user does not have the right privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public HalRepresentation findAll(@PathParam("username") String username) {
|
||||
return collectionMapper.map(username, store.findByUsername(username));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@Produces(MEDIA_TYPE)
|
||||
@Operation(
|
||||
summary = "Get single key for user",
|
||||
description = "Returns a single public key for username by id.",
|
||||
tags = "User",
|
||||
operationId = "get_single_public_key"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = MEDIA_TYPE,
|
||||
schema = @Schema(implementation = RawGpgKeyDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized / the current user does not have the right privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found / key for given id not available",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response findById(@PathParam("id") String id) {
|
||||
Optional<RawGpgKey> byId = store.findById(id);
|
||||
if (byId.isPresent()) {
|
||||
return Response.ok(mapper.map(byId.get())).build();
|
||||
}
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{username}")
|
||||
@Consumes(MEDIA_TYPE)
|
||||
@Operation(
|
||||
summary = "Create new key",
|
||||
description = "Creates new key for user.",
|
||||
tags = "User",
|
||||
operationId = "create_public_key"
|
||||
)
|
||||
@ApiResponse(responseCode = "201", description = "create success")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized / the current user does not have the right privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response create(@Context UriInfo uriInfo, @PathParam("username") String username, RawGpgKeyDto publicKey) {
|
||||
String id = store.add(publicKey.getDisplayName(), username, publicKey.getRaw()).getId();
|
||||
UriBuilder builder = uriInfo.getAbsolutePathBuilder();
|
||||
builder.path(id);
|
||||
return Response.created(builder.build()).build();
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("delete/{id}")
|
||||
@Operation(
|
||||
summary = "Deletes public key",
|
||||
description = "Deletes public key for user.",
|
||||
tags = "User",
|
||||
operationId = "delete_public_key"
|
||||
)
|
||||
@ApiResponse(responseCode = "204", description = "delete success")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized / the current user does not have the right privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response deleteById(@PathParam("id") String id) {
|
||||
store.delete(id);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,18 +24,20 @@
|
||||
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.security.NotPublicKeyException;
|
||||
import sonia.scm.store.DataStore;
|
||||
import sonia.scm.store.DataStoreFactory;
|
||||
import sonia.scm.user.UserPermissions;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Singleton
|
||||
public class PublicKeyStore {
|
||||
@@ -43,26 +45,22 @@ public class PublicKeyStore {
|
||||
private static final String STORE_NAME = "gpg_public_keys";
|
||||
|
||||
private final DataStore<RawGpgKey> store;
|
||||
private final Supplier<String> currentUserSupplier;
|
||||
|
||||
@Inject
|
||||
public PublicKeyStore(DataStoreFactory dataStoreFactory) {
|
||||
this(
|
||||
dataStoreFactory.withType(RawGpgKey.class).withName(STORE_NAME).build(),
|
||||
() -> SecurityUtils.getSubject().getPrincipal().toString()
|
||||
);
|
||||
this.store = dataStoreFactory.withType(RawGpgKey.class).withName(STORE_NAME).build();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
PublicKeyStore(DataStore<RawGpgKey> store, Supplier<String> currentUserSupplier) {
|
||||
this.store = store;
|
||||
this.currentUserSupplier = currentUserSupplier;
|
||||
}
|
||||
public RawGpgKey add(String displayName, String username, String rawKey) {
|
||||
UserPermissions.modify(username).check();
|
||||
|
||||
if (!rawKey.contains("PUBLIC KEY")) {
|
||||
throw new NotPublicKeyException(ContextEntry.ContextBuilder.entity(RawGpgKey.class, displayName).build(), "The provided key is not a public key");
|
||||
}
|
||||
|
||||
public RawGpgKey add(String displayName, String rawKey) {
|
||||
try {
|
||||
String id = Keys.resolveIdFromKey(rawKey);
|
||||
RawGpgKey key = new RawGpgKey(id, displayName, currentUserSupplier.get(), rawKey, Instant.now());
|
||||
RawGpgKey key = new RawGpgKey(id, displayName, username, rawKey, Instant.now());
|
||||
|
||||
store.put(id, key);
|
||||
|
||||
@@ -72,8 +70,23 @@ public class PublicKeyStore {
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(String id) {
|
||||
RawGpgKey rawGpgKey = store.get(id);
|
||||
if (rawGpgKey != null) {
|
||||
UserPermissions.modify(rawGpgKey.getOwner()).check();
|
||||
store.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<RawGpgKey> findById(String id) {
|
||||
return store.getOptional(id);
|
||||
}
|
||||
|
||||
public List<RawGpgKey> findByUsername(String username) {
|
||||
return store.getAll().values()
|
||||
.stream()
|
||||
.filter(rawGpgKey -> username.equalsIgnoreCase(rawGpgKey.getOwner()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import sonia.scm.xml.XmlInstantAdapter;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
@@ -40,6 +41,7 @@ import java.util.Objects;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement
|
||||
public class RawGpgKey {
|
||||
|
||||
private String id;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("squid:S2160") // we do not need equals for dto
|
||||
public class RawGpgKeyDto extends HalRepresentation {
|
||||
|
||||
private String displayName;
|
||||
private String raw;
|
||||
private Instant created;
|
||||
|
||||
RawGpgKeyDto(Links links) {
|
||||
super(links);
|
||||
}
|
||||
}
|
||||
@@ -198,6 +198,28 @@ class MeDtoFactoryTest {
|
||||
assertThat(dto.getLinks().getLinkBy("password")).isNotPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAppendPublicKeysLink() {
|
||||
User user = UserTestData.createTrillian();
|
||||
prepareSubject(user);
|
||||
|
||||
when(subject.isPermitted("user:changePublicKeys:trillian")).thenReturn(true);
|
||||
|
||||
MeDto dto = meDtoFactory.create();
|
||||
assertThat(dto.getLinks().getLinkBy("publicKeys").get().getHref()).isEqualTo("https://scm.hitchhiker.com/scm/v2/public_keys/trillian");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotAppendPublicKeysLink() {
|
||||
User user = UserTestData.createTrillian();
|
||||
prepareSubject(user);
|
||||
|
||||
when(subject.isPermitted("user:changePublicKeys:trillian")).thenReturn(false);
|
||||
|
||||
MeDto dto = meDtoFactory.create();
|
||||
assertThat(dto.getLinks().getLinkBy("publicKeys")).isNotPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAppendLinks() {
|
||||
prepareSubject(UserTestData.createTrillian());
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.inject.util.Providers;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.api.v2.resources.ScmPathInfoStore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PublicKeyCollectionMapperTest {
|
||||
|
||||
|
||||
private PublicKeyCollectionMapper collectionMapper;
|
||||
|
||||
@Mock
|
||||
private PublicKeyMapper mapper;
|
||||
|
||||
@Mock
|
||||
private Subject subject;
|
||||
|
||||
@BeforeEach
|
||||
void setUpObjectUnderTest() {
|
||||
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
|
||||
pathInfoStore.set(() -> URI.create("/"));
|
||||
collectionMapper = new PublicKeyCollectionMapper(Providers.of(pathInfoStore), mapper);
|
||||
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanThreadContext() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapToCollection() throws IOException {
|
||||
when(mapper.map(any(RawGpgKey.class))).then(ic -> new RawGpgKeyDto());
|
||||
|
||||
RawGpgKey one = createPublicKey("one");
|
||||
RawGpgKey two = createPublicKey("two");
|
||||
|
||||
List<RawGpgKey> keys = Lists.newArrayList(one, two);
|
||||
HalRepresentation collection = collectionMapper.map("trillian", keys);
|
||||
|
||||
List<HalRepresentation> embedded = collection.getEmbedded().getItemsBy("keys");
|
||||
assertThat(embedded).hasSize(2);
|
||||
|
||||
assertThat(collection.getLinks().getLinkBy("self").get().getHref()).isEqualTo("/v2/public_keys/trillian");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAddCreateLinkIfTheUserIsPermitted() {
|
||||
when(subject.isPermitted("user:changePublicKeys:trillian")).thenReturn(true);
|
||||
|
||||
HalRepresentation collection = collectionMapper.map("trillian", Lists.newArrayList());
|
||||
assertThat(collection.getLinks().getLinkBy("create").get().getHref()).isEqualTo("/v2/public_keys/trillian");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotAddCreateLinkWithoutPermission() {
|
||||
HalRepresentation collection = collectionMapper.map("trillian", Lists.newArrayList());
|
||||
assertThat(collection.getLinks().getLinkBy("create")).isNotPresent();
|
||||
}
|
||||
|
||||
private RawGpgKey createPublicKey(String displayName) throws IOException {
|
||||
String raw = GPGTestHelper.readKey("single.asc");
|
||||
return new RawGpgKey(displayName, displayName, "trillian", raw, Instant.now());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import com.google.inject.util.Providers;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.api.v2.resources.ScmPathInfoStore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PublicKeyMapperTest {
|
||||
|
||||
@Mock
|
||||
private Subject subject;
|
||||
|
||||
private final PublicKeyMapper mapper = new PublicKeyMapperImpl();
|
||||
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
ThreadContext.bind(subject);
|
||||
|
||||
pathInfoStore.set(() -> URI.create("/"));
|
||||
mapper.setScmPathInfoStore(Providers.of(pathInfoStore));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDownSubject() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapKeyToDto() throws IOException {
|
||||
when(subject.isPermitted("user:changePublicKeys:trillian")).thenReturn(true);
|
||||
|
||||
String raw = GPGTestHelper.readKey("single.asc");
|
||||
RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Instant.now());
|
||||
|
||||
RawGpgKeyDto dto = mapper.map(key);
|
||||
|
||||
assertThat(dto.getDisplayName()).isEqualTo(key.getDisplayName());
|
||||
assertThat(dto.getRaw()).isEqualTo(key.getRaw());
|
||||
assertThat(dto.getCreated()).isEqualTo(key.getCreated());
|
||||
assertThat(dto.getLinks().getLinkBy("self").get().getHref()).isEqualTo("/v2/public_keys/1");
|
||||
assertThat(dto.getLinks().getLinkBy("delete").get().getHref()).isEqualTo("/v2/public_keys/delete/1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotAppendDeleteLink() throws IOException {
|
||||
String raw = GPGTestHelper.readKey("single.asc");
|
||||
RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Instant.now());
|
||||
|
||||
RawGpgKeyDto dto = mapper.map(key);
|
||||
|
||||
assertThat(dto.getLinks().getLinkBy("delete")).isNotPresent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PublicKeyResourceTest {
|
||||
|
||||
@Mock
|
||||
private PublicKeyStore store;
|
||||
|
||||
@Mock
|
||||
private PublicKeyCollectionMapper collectionMapper;
|
||||
|
||||
@Mock
|
||||
private PublicKeyMapper mapper;
|
||||
|
||||
@InjectMocks
|
||||
private PublicKeyResource resource;
|
||||
|
||||
@Mock
|
||||
private Subject subject;
|
||||
|
||||
@BeforeEach
|
||||
void setUpSubject() {
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void clearSubject() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindAll() {
|
||||
List<RawGpgKey> keys = new ArrayList<>();
|
||||
when(store.findByUsername("trillian")).thenReturn(keys);
|
||||
|
||||
HalRepresentation collection = new HalRepresentation();
|
||||
when(collectionMapper.map("trillian", keys)).thenReturn(collection);
|
||||
|
||||
HalRepresentation result = resource.findAll("trillian");
|
||||
assertThat(result).isSameAs(collection);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindById() {
|
||||
RawGpgKey key = new RawGpgKey("42");
|
||||
when(store.findById("42")).thenReturn(Optional.of(key));
|
||||
RawGpgKeyDto dto = new RawGpgKeyDto();
|
||||
when(mapper.map(key)).thenReturn(dto);
|
||||
|
||||
Response response = resource.findById("42");
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getEntity()).isSameAs(dto);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturn404IfIdDoesNotExists() {
|
||||
when(store.findById("42")).thenReturn(Optional.empty());
|
||||
|
||||
Response response = resource.findById("42");
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAddToStore() throws URISyntaxException, IOException {
|
||||
String raw = GPGTestHelper.readKey("single.asc");
|
||||
|
||||
UriInfo uriInfo = mock(UriInfo.class);
|
||||
UriBuilder builder = mock(UriBuilder.class);
|
||||
when(uriInfo.getAbsolutePathBuilder()).thenReturn(builder);
|
||||
when(builder.path("42")).thenReturn(builder);
|
||||
when(builder.build()).thenReturn(new URI("/v2/public_keys/42"));
|
||||
|
||||
RawGpgKey key = new RawGpgKey("42");
|
||||
RawGpgKeyDto dto = new RawGpgKeyDto();
|
||||
dto.setDisplayName("key_42");
|
||||
dto.setRaw(raw);
|
||||
when(store.add(dto.getDisplayName(), "trillian", dto.getRaw())).thenReturn(key);
|
||||
|
||||
Response response = resource.create(uriInfo, "trillian", dto);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(201);
|
||||
assertThat(response.getLocation().toASCIIString()).isEqualTo("/v2/public_keys/42");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteFromStore() {
|
||||
Response response = resource.deleteById("42");
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
verify(store).delete("42");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,23 +24,65 @@
|
||||
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import sonia.scm.store.InMemoryDataStore;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.security.NotPublicKeyException;
|
||||
import sonia.scm.store.DataStoreFactory;
|
||||
import sonia.scm.store.InMemoryDataStoreFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PublicKeyStoreTest {
|
||||
|
||||
@Mock
|
||||
private Subject subject;
|
||||
|
||||
private PublicKeyStore keyStore;
|
||||
private final DataStoreFactory dataStoreFactory = new InMemoryDataStoreFactory();
|
||||
|
||||
@BeforeEach
|
||||
void setUpKeyStore() {
|
||||
keyStore = new PublicKeyStore(new InMemoryDataStore<>(), () -> "trillian");
|
||||
keyStore = new PublicKeyStore(dataStoreFactory);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void bindSubject() {
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDownSubject() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowAuthorizationExceptionOnAdd() throws IOException {
|
||||
doThrow(AuthorizationException.class).when(subject).checkPermission("user:modify:zaphod");
|
||||
String rawKey = GPGTestHelper.readKey("single.asc");
|
||||
|
||||
assertThrows(AuthorizationException.class, () -> keyStore.add("zaphods key", "zaphod", rawKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOnlyStorePublicKeys() throws IOException {
|
||||
String rawKey = GPGTestHelper.readKey("single.asc").replace("PUBLIC", "PRIVATE");
|
||||
|
||||
assertThrows(NotPublicKeyException.class, () -> keyStore.add("SCM Package Key", "trillian", rawKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -48,7 +90,7 @@ class PublicKeyStoreTest {
|
||||
String rawKey = GPGTestHelper.readKey("single.asc");
|
||||
Instant now = Instant.now();
|
||||
|
||||
RawGpgKey key = keyStore.add("SCM Package Key", rawKey);
|
||||
RawGpgKey key = keyStore.add("SCM Package Key", "trillian", rawKey);
|
||||
assertThat(key.getId()).isEqualTo("0x975922F193B07D6E");
|
||||
assertThat(key.getDisplayName()).isEqualTo("SCM Package Key");
|
||||
assertThat(key.getOwner()).isEqualTo("trillian");
|
||||
@@ -59,9 +101,44 @@ class PublicKeyStoreTest {
|
||||
@Test
|
||||
void shouldFindStoredKeyById() throws IOException {
|
||||
String rawKey = GPGTestHelper.readKey("single.asc");
|
||||
keyStore.add("SCM Package Key", rawKey);
|
||||
keyStore.add("SCM Package Key", "trillian", rawKey);
|
||||
Optional<RawGpgKey> key = keyStore.findById("0x975922F193B07D6E");
|
||||
assertThat(key).isPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteKey() throws IOException {
|
||||
String rawKey = GPGTestHelper.readKey("single.asc");
|
||||
keyStore.add("SCM Package Key", "trillian", rawKey);
|
||||
Optional<RawGpgKey> key = keyStore.findById("0x975922F193B07D6E");
|
||||
|
||||
assertThat(key).isPresent();
|
||||
|
||||
keyStore.delete("0x975922F193B07D6E");
|
||||
key = keyStore.findById("0x975922F193B07D6E");
|
||||
|
||||
assertThat(key).isNotPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyListIfNoKeysAvailable() {
|
||||
List<RawGpgKey> keys = keyStore.findByUsername("zaphod");
|
||||
|
||||
assertThat(keys).isEmpty();
|
||||
assertThat(keys).isInstanceOf(List.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindAllKeysForUser() throws IOException {
|
||||
String singleKey = GPGTestHelper.readKey("single.asc");
|
||||
keyStore.add("SCM Single Key", "trillian", singleKey);
|
||||
|
||||
String multiKey = GPGTestHelper.readKey("subkeys.asc");
|
||||
keyStore.add("SCM Multi Key", "trillian", multiKey);
|
||||
|
||||
List<RawGpgKey> keys = keyStore.findByUsername("trillian");
|
||||
|
||||
assertThat(keys.size()).isEqualTo(2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user