Skip to content

Commit

Permalink
fix(auth)!: 拆分getUser为独立的认证系统能力 (#314)
Browse files Browse the repository at this point in the history
经过进一步考虑,发现getUser应该是一个独立的认证系统能力。有的认证系统不支持创建用户,但是支持查询用户是否存在,比如SSH。有的认证系统支持创建用户,但是不支持查询用户是否存在,比如一些机构的内部认证系统,内部认证系统基本不会给接入者随便查询任意用户的能力。所以getUser应该不是和创建用户的绑定的接口,而是一个独立的能力。
  • Loading branch information
ddadaal committed Nov 27, 2022
1 parent 9315a08 commit 911c5f0
Show file tree
Hide file tree
Showing 13 changed files with 233 additions and 182 deletions.
6 changes: 2 additions & 4 deletions apps/auth/src/auth/AuthProvider.ts
Expand Up @@ -17,10 +17,8 @@ export type ChangePasswordResult = "NotFound" | "WrongOldPassword" | "OK";
export interface AuthProvider {
serveLoginHtml: (callbackUrl: string, req: FastifyRequest, rep: FastifyReply) => Promise<void>;
fetchAuthTokenInfo: (token: string, req: FastifyRequest) => Promise<string | undefined>;
createUser: {
createUser: ((info: CreateUserInfo, req: FastifyRequest) => Promise<CreateUserResult>);
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<CreateUserResult>);
validateName: undefined | ((identityId: string, name: string, req: FastifyRequest) => Promise<ValidateNameResult>);
changePassword: undefined | ((id: string, oldPassword: string, newPassword: string,
req: FastifyRequest) => Promise<ChangePasswordResult>);
Expand Down
142 changes: 142 additions & 0 deletions 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<string, string | string[]>, 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<AuthConfigSchema["ldap"]>,
): Promise<CreateUserResult> {

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<string, string | string[] | number> = {
[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";

}
145 changes: 8 additions & 137 deletions 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<string, string | string[]>, 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) => {

Expand Down Expand Up @@ -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<string, string | string[] | number> = {
[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) => {
Expand Down
9 changes: 9 additions & 0 deletions 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) {
Expand Down Expand Up @@ -38,6 +40,13 @@ export const createSshAuthProvider = (f: FastifyInstance) => {
return <AuthProvider>{
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,
Expand Down
2 changes: 2 additions & 0 deletions apps/auth/src/routes/capabilities.ts
Expand Up @@ -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<typeof CapabilitiesSchema>;
Expand Down Expand Up @@ -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,
};
},
);
Expand Down
2 changes: 1 addition & 1 deletion apps/auth/src/routes/createUser.ts
Expand Up @@ -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();
},
Expand Down
4 changes: 2 additions & 2 deletions apps/auth/src/routes/getUser.ts
Expand Up @@ -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 } });
Expand Down

0 comments on commit 911c5f0

Please sign in to comment.