diff --git a/apps/auth/src/auth/AuthProvider.ts b/apps/auth/src/auth/AuthProvider.ts index c5e82447af..001c42b921 100644 --- a/apps/auth/src/auth/AuthProvider.ts +++ b/apps/auth/src/auth/AuthProvider.ts @@ -17,10 +17,8 @@ export type ChangePasswordResult = "NotFound" | "WrongOldPassword" | "OK"; export interface AuthProvider { serveLoginHtml: (callbackUrl: string, req: FastifyRequest, rep: FastifyReply) => Promise; fetchAuthTokenInfo: (token: string, req: FastifyRequest) => Promise; - createUser: { - createUser: ((info: CreateUserInfo, req: FastifyRequest) => Promise); - getUser: (identityId: string, req: FastifyRequest) => Promise<{ identityId: string } | undefined>; - } | undefined, + getUser: undefined | ((identityId: string, req: FastifyRequest) => Promise<{ identityId: string } | undefined>); + createUser: undefined | ((info: CreateUserInfo, req: FastifyRequest) => Promise); validateName: undefined | ((identityId: string, name: string, req: FastifyRequest) => Promise); changePassword: undefined | ((id: string, oldPassword: string, newPassword: string, req: FastifyRequest) => Promise); diff --git a/apps/auth/src/auth/ldap/createUser.ts b/apps/auth/src/auth/ldap/createUser.ts new file mode 100644 index 0000000000..8fa520fd3a --- /dev/null +++ b/apps/auth/src/auth/ldap/createUser.ts @@ -0,0 +1,142 @@ +import { parsePlaceholder } from "@scow/lib-config"; +import { FastifyRequest } from "fastify"; +import ldapjs from "ldapjs"; +import { CreateUserInfo, CreateUserResult } from "src/auth/AuthProvider"; +import { searchOne, useLdap } from "src/auth/ldap/helpers"; +import { modifyPassword } from "src/auth/ldap/password"; +import { AuthConfigSchema, NewUserGroupStrategy } from "src/config/auth"; +import { promisify } from "util"; + +/* + * Apply extra props. + * @param obj the object to apply extra props + * @param extraProps the extraProps config + * @param placeholderObj the object where the values of placeholders ({{ }}) are from + */ +const applyExtraProps = (obj: object, extraProps: Record, placeholderObj: object) => { + + for (const key in extraProps) { + const value = extraProps[key]; + if (Array.isArray(value)) { + obj[key] = value.map((x) => parsePlaceholder(x, placeholderObj)); + } else { + obj[key] = parsePlaceholder(value, placeholderObj); + } + } + +}; + +export async function createUser( + info: CreateUserInfo, req: FastifyRequest, ldap: NonNullable, +): Promise { + + const id = info.id + ldap.addUser.uidStart; + + await useLdap(req.log, ldap)(async (client) => { + const userDn = + `${ldap.addUser.userIdDnKey ?? ldap.attrs.uid}=${info.identityId},` + + `${ldap.addUser.userBase}`; + const userEntry: Record = { + [ldap.attrs.uid]: info.identityId, + sn: info.identityId, + loginShell: "/bin/bash", + objectClass: ["inetOrgPerson", "posixAccount", "shadowAccount"], + homeDirectory: parsePlaceholder(ldap.addUser.homeDir, { userId: info.identityId }), + uidNumber: id, + gidNumber: id, + }; + + if (ldap.attrs.name) { + userEntry[ldap.attrs.name] = info.name; + } + + if (ldap.attrs.mail) { + userEntry[ldap.attrs.mail] = info.mail; + } + + // parse attributes + if (ldap.addUser.extraProps) { + applyExtraProps(userEntry, ldap.addUser.extraProps, userEntry); + } + + const add = promisify(client.add.bind(client)); + + + if (ldap.addUser.groupStrategy === NewUserGroupStrategy.newGroupPerUser) { + + req.log.info("ldap.addUser.groupStrategy is newGroupPerUser. Creating new group for the user."); + + const config = ldap.addUser.newGroupPerUser!; + + const groupDn = `${config.groupIdDnKey ?? ldap.attrs.uid}=${info.identityId},${config.groupBase}`; + const groupEntry = { + objectClass: ["posixGroup"], + memberUid: info.identityId, + gidNumber: id, + }; + + userEntry["gidNumber"] = id; + + if (config.extraProps) { + applyExtraProps(groupEntry, config.extraProps, userEntry); + } + + req.log.info("Adding group %s with entry info %o", groupDn, groupEntry); + await add(groupDn, groupEntry); + + } + + if (ldap.addUser.groupStrategy === NewUserGroupStrategy.oneGroupForAllUsers) { + const config = ldap.addUser.oneGroupForAllUsers!; + + req.log.info("ldap.addUser.groupStrategy is one-group-for-all-users."); + req.log.info("Using existing group %s for the user", config.gidNumber); + + userEntry["gidNumber"] = config.gidNumber; + } + + req.log.info("Adding people %s with entry info %o", userDn, userEntry); + await add(userDn, userEntry); + + // set password as admin user + await modifyPassword(userDn, undefined, info.password, client); + + // Add user to ldap group + const addUserToLdapGroup = ldap.addUser.addUserToLdapGroup; + + if (addUserToLdapGroup) { + // get existing members + req.log.info("Adding %s to group %s", userDn, addUserToLdapGroup); + + const members = await searchOne(req.log, client, addUserToLdapGroup, { + attributes: ["member"], + }, (entry) => { + const member = entry.attributes.find((x) => x.json.type === "member"); + if (!member) { + return undefined; + } + + return { members: member.json.vals }; + }); + + if (!members) { + req.log.error("Didn't find LDAP group %s", addUserToLdapGroup); + throw { code: "INTERNAL_ERROR" }; + } + + // add the dn of the new user to the value + const modify = promisify(client.modify.bind(client)); + await modify(addUserToLdapGroup, new ldapjs.Change({ + operation: "add", + modification: { + "member": members.members.concat(userDn), + }, + })); + } + + + }); + + return "OK"; + +} diff --git a/apps/auth/src/auth/ldap/index.ts b/apps/auth/src/auth/ldap/index.ts index aa7fdb04de..9b11fe4ab9 100644 --- a/apps/auth/src/auth/ldap/index.ts +++ b/apps/auth/src/auth/ldap/index.ts @@ -1,33 +1,13 @@ -import { parsePlaceholder } from "@scow/lib-config"; import { FastifyInstance } from "fastify"; import ldapjs from "ldapjs"; import { AuthProvider } from "src/auth/AuthProvider"; +import { createUser } from "src/auth/ldap/createUser"; import { extractUserInfoFromEntry, findUser, searchOne, useLdap } from "src/auth/ldap/helpers"; -import { modifyPassword, modifyPasswordAsSelf } from "src/auth/ldap/password"; +import { modifyPasswordAsSelf } from "src/auth/ldap/password"; import { registerPostHandler } from "src/auth/ldap/postHandler"; import { serveLoginHtml } from "src/auth/loginHtml"; -import { authConfig, NewUserGroupStrategy } from "src/config/auth"; +import { authConfig } from "src/config/auth"; import { ensureNotUndefined } from "src/utils/validations"; -import { promisify } from "util"; - -/** - * Apply extra props. - * @param obj the object to apply extra props - * @param extraProps the extraProps config - * @param placeholderObj the object where the values of placeholders ({{ }}) are from - */ -const applyExtraProps = (obj: object, extraProps: Record, placeholderObj: object) => { - - for (const key in extraProps) { - const value = extraProps[key]; - if (Array.isArray(value)) { - obj[key] = value.map((x) => parsePlaceholder(x, placeholderObj)); - } else { - obj[key] = parsePlaceholder(value, placeholderObj); - } - } - -}; export const createLdapAuthProvider = (f: FastifyInstance) => { @@ -68,120 +48,11 @@ export const createLdapAuthProvider = (f: FastifyInstance) => { return "Match"; }); } : undefined, - createUser: { - getUser: async (identityId, req) => useLdap(req.log, ldap)(async (client) => ( - findUser(req.log, ldap, client, identityId) - )), - createUser: async (info, req) => { - const id = info.id + ldap.addUser.uidStart; - - await useLdap(req.log, ldap)(async (client) => { - const userDn = - `${ldap.addUser.userIdDnKey ?? ldap.attrs.uid}=${info.identityId},` + - `${ldap.addUser.userBase}`; - const userEntry: Record = { - [ldap.attrs.uid]: info.identityId, - sn: info.identityId, - loginShell: "/bin/bash", - objectClass: ["inetOrgPerson", "posixAccount", "shadowAccount"], - homeDirectory: parsePlaceholder(ldap.addUser.homeDir, { userId: info.identityId }), - uidNumber: id, - gidNumber: id, - }; - - if (ldap.attrs.name) { - userEntry[ldap.attrs.name] = info.name; - } - - if (ldap.attrs.mail) { - userEntry[ldap.attrs.mail] = info.mail; - } - - // parse attributes - if (ldap.addUser.extraProps) { - applyExtraProps(userEntry, ldap.addUser.extraProps, userEntry); - } - - const add = promisify(client.add.bind(client)); - - - if (ldap.addUser.groupStrategy === NewUserGroupStrategy.newGroupPerUser) { - - req.log.info("ldap.addUser.groupStrategy is newGroupPerUser. Creating new group for the user."); - - const config = ldap.addUser.newGroupPerUser!; - - const groupDn = `${config.groupIdDnKey ?? ldap.attrs.uid}=${info.identityId},${config.groupBase}`; - const groupEntry = { - objectClass: ["posixGroup"], - memberUid: info.identityId, - gidNumber: id, - }; - - userEntry["gidNumber"] = id; - - if (config.extraProps) { - applyExtraProps(groupEntry, config.extraProps, userEntry); - } - - req.log.info("Adding group %s with entry info %o", groupDn, groupEntry); - await add(groupDn, groupEntry); - - } - - if (ldap.addUser.groupStrategy === NewUserGroupStrategy.oneGroupForAllUsers) { - const config = ldap.addUser.oneGroupForAllUsers!; - - req.log.info("ldap.addUser.groupStrategy is one-group-for-all-users."); - req.log.info("Using existing group %s for the user", config.gidNumber); - - userEntry["gidNumber"] = config.gidNumber; - } - - req.log.info("Adding people %s with entry info %o", userDn, userEntry); - await add(userDn, userEntry); - - // set password as admin user - await modifyPassword(userDn, undefined, info.password, client); - - // Add user to ldap group - const addUserToLdapGroup = ldap.addUser.addUserToLdapGroup; - - if (addUserToLdapGroup) { - // get existing members - req.log.info("Adding %s to group %s", userDn, addUserToLdapGroup); - - const members = await searchOne(req.log, client, addUserToLdapGroup, { - attributes: ["member"], - }, (entry) => { - const member = entry.attributes.find((x) => x.json.type === "member"); - if (!member) { - return undefined; - } - - return { members: member.json.vals }; - }); - - if (!members) { - req.log.error("Didn't find LDAP group %s", addUserToLdapGroup); - throw { code: "INTERNAL_ERROR" }; - } - - // add the dn of the new user to the value - const modify = promisify(client.modify.bind(client)); - await modify(addUserToLdapGroup, new ldapjs.Change({ - operation: "add", - modification: { - "member": members.members.concat(userDn), - }, - })); - } - - - }); - - return "OK"; - }, + getUser: async (identityId, req) => useLdap(req.log, ldap)(async (client) => ( + findUser(req.log, ldap, client, identityId) + )), + createUser: async (info, req) => { + return createUser(info, req, ldap); }, changePassword: async (id, oldPassword, newPassword, req) => { return useLdap(req.log, ldap)(async (client) => { diff --git a/apps/auth/src/auth/ssh/index.ts b/apps/auth/src/auth/ssh/index.ts index deae2f9d02..87029ba85a 100644 --- a/apps/auth/src/auth/ssh/index.ts +++ b/apps/auth/src/auth/ssh/index.ts @@ -1,9 +1,11 @@ +import { loggedExec, sshConnect } from "@scow/lib-ssh"; import { FastifyInstance } from "fastify"; import { AuthProvider } from "src/auth/AuthProvider"; import { serveLoginHtml } from "src/auth/loginHtml"; import { registerPostHandler } from "src/auth/ssh/postHandler"; import { authConfig, SshConfigSchema } from "src/config/auth"; import { clusters } from "src/config/clusters"; +import { rootKeyPair } from "src/config/env"; import { ensureNotUndefined } from "src/utils/validations"; function checkLoginNode(sshConfig: SshConfigSchema) { @@ -38,6 +40,13 @@ export const createSshAuthProvider = (f: FastifyInstance) => { return { serveLoginHtml: (callbackUrl, req, rep) => serveLoginHtml(false, callbackUrl, req, rep), fetchAuthTokenInfo: async () => undefined, + getUser: async (identityId, req) => { + return await sshConnect(loginNode, "root", rootKeyPair, req.log, async (ssh) => { + return loggedExec(ssh, req.log, true, "id", [identityId]) + .then(() => ({ identityId })) + .catch(() => undefined); + }); + }, validateName: undefined, createUser: undefined, changePassword: undefined, diff --git a/apps/auth/src/routes/capabilities.ts b/apps/auth/src/routes/capabilities.ts index 7236e9989e..803733356c 100644 --- a/apps/auth/src/routes/capabilities.ts +++ b/apps/auth/src/routes/capabilities.ts @@ -5,6 +5,7 @@ const CapabilitiesSchema = Type.Object({ createUser: Type.Boolean({ description: "是否可以创建用户" }), changePassword: Type.Boolean({ description: "是否可以修改密码" }), validateName: Type.Boolean({ description: "是否可以验证用户名的密码" }), + getUser: Type.Boolean({ description: "是否可以查询用户" }), }); export type Capabilities = Static; @@ -33,6 +34,7 @@ export const getCapabilitiesRoute = fp(async (f) => { createUser: provider.createUser !== undefined, validateName: provider.validateName !== undefined, changePassword: provider.changePassword !== undefined, + getUser: provider.getUser !== undefined, }; }, ); diff --git a/apps/auth/src/routes/createUser.ts b/apps/auth/src/routes/createUser.ts index 2a5d71dbee..5feb860c53 100644 --- a/apps/auth/src/routes/createUser.ts +++ b/apps/auth/src/routes/createUser.ts @@ -48,7 +48,7 @@ export const createUserRoute = fp(async (f) => { const { ...rest } = req.body; - const result = await f.auth.createUser.createUser(rest, req); + const result = await f.auth.createUser(rest, req); return await rep.status(codes[result]).send(); }, diff --git a/apps/auth/src/routes/getUser.ts b/apps/auth/src/routes/getUser.ts index e482c2b3ae..7b45355d20 100644 --- a/apps/auth/src/routes/getUser.ts +++ b/apps/auth/src/routes/getUser.ts @@ -27,13 +27,13 @@ export const getUserRoute = fp(async (f) => { }, }, async (req, rep) => { - if (!f.auth.createUser) { + if (!f.auth.getUser) { return await rep.code(501).send(null); } const { identityId } = req.query; - const result = await f.auth.createUser.getUser(identityId, req); + const result = await f.auth.getUser(identityId, req); if (result) { return rep.code(200).send({ user: { identityId: result.identityId } }); diff --git a/apps/auth/tests/ssh.test.ts b/apps/auth/tests/ssh.test.ts index 2553e732ec..177842117e 100644 --- a/apps/auth/tests/ssh.test.ts +++ b/apps/auth/tests/ssh.test.ts @@ -60,3 +60,25 @@ it("fails to login with wrong credentials", async () => { expect(resp.statusCode).toBe(403); }); + +it("gets user info", async () => { + const resp = await server.inject({ + method: "GET", + url: "/user", + query: { identityId: username }, + }); + + expect(resp.statusCode).toBe(200); + expect(resp.json()).toEqual({ user: { identityId: username } }); +}); + +it("returns 404 if user doesn't exist", async () => { + const resp = await server.inject({ + method: "GET", + url: "/user", + query: { identityId: username + "wrong" }, + }); + + expect(resp.statusCode).toBe(404); + expect(resp.json()).toEqual({ code: "USER_NOT_FOUND" }); +}); diff --git a/docs/docs/common/auth/custom/implementation.md b/docs/docs/common/auth/custom/implementation.md index 269b2bbe95..0df89e008d 100644 --- a/docs/docs/common/auth/custom/implementation.md +++ b/docs/docs/common/auth/custom/implementation.md @@ -103,6 +103,7 @@ SCOW中使用`identityId`标识一个用户,并同时使用此`identityId`作 | 字段 | 类型 | 是否必须 | 解释 | | ---------------- | ------- | -------- | ------------------------------------ | | `createUser` | boolean | 是 | 此认证系统是否支持创建用户 | +| `getUser` | boolean | 是 | 此认证系统是否支持查询用户 | | `changePassword` | boolean | 是 | 此认证系统是否支持修改用户密码 | | `validateName` | boolean | 是 | 此认证系统是否支持验证用户名和其姓名 | @@ -140,6 +141,8 @@ SCOW中使用`identityId`标识一个用户,并同时使用此`identityId`作 此API用于在认证系统中创建用户。当前,系统只支持通过管理系统创建用户。管理系统首先在自己的数据库中创建用户,然后请求认证系统创建用户。请求参数中的`id`即是数据库中这个新的用户的自增ID。如果认证系统返回非成功的返回值,管理系统将会撤回在数据库中的项。 +## 查询用户功能相关API + ### GET /user 获取已经存在的用户信息。目前只需要返回用户的ID。 diff --git a/libs/auth/src/createUser.ts b/libs/auth/src/createUser.ts index bef255e8ea..4f9531d159 100644 --- a/libs/auth/src/createUser.ts +++ b/libs/auth/src/createUser.ts @@ -1,4 +1,3 @@ -import { join } from "path"; import { applicationJsonHeaders, logHttpErrorAndThrow } from "src/utils"; import { Logger } from "ts-log"; @@ -19,39 +18,3 @@ export async function createUser( } -export interface AuthUserInfo { - identityId: string; -} - -/** - * Get user info - * @param authUrl the url for auth service - * @param params the API parameters - * @returns the user info. undefined if user do not exist - */ -export async function getUser( - authUrl: string, - params: { identityId: string }, - logger?: Logger, -): Promise { - - const query = new URLSearchParams([["identityId", params.identityId]]); - const url = join(authUrl, "/user") + "?" + query.toString(); - const resp = await fetch(url, { - headers: applicationJsonHeaders, - }); - - if (resp.status === 200) { - return await resp.json(); - } else if (resp.status === 404) { - const json = await resp.json().catch(() => undefined); - - if (json?.code === "USER_NOT_FOUND") { - return undefined; - } else { - logHttpErrorAndThrow(resp, logger); - } - } else { - logHttpErrorAndThrow(resp, logger); - } -} diff --git a/libs/auth/src/getCapabilities.ts b/libs/auth/src/getCapabilities.ts index fd53099f82..7ef54bc27f 100644 --- a/libs/auth/src/getCapabilities.ts +++ b/libs/auth/src/getCapabilities.ts @@ -4,6 +4,7 @@ export interface Capabilities { createUser: boolean; changePassword: boolean; validateName: boolean; + getUser: boolean; } diff --git a/libs/auth/src/getUser.ts b/libs/auth/src/getUser.ts new file mode 100644 index 0000000000..d6daa687c4 --- /dev/null +++ b/libs/auth/src/getUser.ts @@ -0,0 +1,40 @@ +import { join } from "path"; +import { applicationJsonHeaders, logHttpErrorAndThrow } from "src/utils"; +import { Logger } from "ts-log"; + +export interface AuthUserInfo { + identityId: string; +} + +/** + * Get user info + * @param authUrl the url for auth service + * @param params the API parameters + * @returns the user info. undefined if user do not exist + */ +export async function getUser( + authUrl: string, + params: { identityId: string }, + logger?: Logger, +): Promise { + + const query = new URLSearchParams([["identityId", params.identityId]]); + const url = join(authUrl, "/user") + "?" + query.toString(); + const resp = await fetch(url, { + headers: applicationJsonHeaders, + }); + + if (resp.status === 200) { + return await resp.json(); + } else if (resp.status === 404) { + const json = await resp.json().catch(() => undefined); + + if (json?.code === "USER_NOT_FOUND") { + return undefined; + } else { + logHttpErrorAndThrow(resp, logger); + } + } else { + logHttpErrorAndThrow(resp, logger); + } +} diff --git a/libs/auth/tests/getUser.test.ts b/libs/auth/tests/getUser.test.ts index 2022349555..0059ce1f2d 100644 --- a/libs/auth/tests/getUser.test.ts +++ b/libs/auth/tests/getUser.test.ts @@ -1,4 +1,4 @@ -import { getUser } from "src/createUser"; +import { getUser } from "src/getUser"; import { applicationJsonHeaders } from "src/utils"; import { mockFetch } from "tests/utils";